[SIEM][Exceptions] - Added exception list hooks for UI #67300

Added some basic functionality to help exception list UI work move forward. Wired up to exception list api and created hooks. This PR includes:

- UI api functions for basic exception list and exception list item CRUD
- useExceptionList hook to fetch the list and its items
- usePersistExceptionList hook to create or update an exception list
- usePersistExceptionListItem hook to create or update an exception item
- list_plugin_deps.tsx in the siem folder to import the lists plugin hooks
This commit is contained in:
Yara Tercero 2020-05-26 10:36:06 -04:00 committed by GitHub
parent c9c37f77e6
commit 3c48b3acd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1104 additions and 0 deletions

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { coreMock } from '../../../../../../src/core/public/mocks';
import { CoreStart } from '../../../../../../src/core/public';
export type GlobalServices = Pick<CoreStart, 'http'>;
export const createKibanaCoreStartMock = (): GlobalServices => coreMock.createStart();

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
} from '../../../common/schemas';
import {
AddExceptionListItemProps,
AddExceptionListProps,
ApiCallByIdProps,
ApiCallByListIdProps,
} from '../types';
import { mockExceptionItem, mockExceptionList } from '../mock';
/* eslint-disable @typescript-eslint/no-unused-vars */
export const addExceptionList = async ({
http,
list,
signal,
}: AddExceptionListProps): Promise<ExceptionListSchema> => Promise.resolve(mockExceptionList);
export const addExceptionListItem = async ({
http,
listItem,
signal,
}: AddExceptionListItemProps): Promise<ExceptionListItemSchema> =>
Promise.resolve(mockExceptionItem);
export const fetchExceptionListById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> => Promise.resolve(mockExceptionList);
export const fetchExceptionListItemsByListId = async ({
http,
listId,
signal,
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> =>
Promise.resolve({ data: [mockExceptionItem], page: 1, per_page: 20, total: 1 });
export const fetchExceptionListItemById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> => Promise.resolve(mockExceptionItem);

View file

@ -0,0 +1,285 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createKibanaCoreStartMock } from '../common/mocks/kibana_core';
import {
mockExceptionItem,
mockExceptionList,
mockNewExceptionItem,
mockNewExceptionList,
} from './mock';
import {
addExceptionList,
addExceptionListItem,
deleteExceptionListById,
deleteExceptionListItemById,
fetchExceptionListById,
fetchExceptionListItemById,
fetchExceptionListItemsByListId,
} from './api';
const abortCtrl = new AbortController();
jest.mock('../common/mocks/kibana_core', () => ({
createKibanaCoreStartMock: (): jest.Mock => jest.fn(),
}));
const fetchMock = jest.fn();
/*
This is a little funky, in order for typescript to not
yell at us for converting 'Pick<CoreStart, "http">' to type 'Mock<any, any>'
have to first convert to type 'unknown'
*/
const mockKibanaHttpService = ((createKibanaCoreStartMock() as unknown) as jest.Mock).mockReturnValue(
{
fetch: fetchMock,
}
);
describe('Exceptions Lists API', () => {
describe('addExceptionList', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(mockExceptionList);
});
test('check parameter url, body', async () => {
await addExceptionList({
http: mockKibanaHttpService(),
list: mockNewExceptionList,
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body:
'{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"endpoint"}',
method: 'POST',
signal: abortCtrl.signal,
});
});
test('check parameter url, body when "list.id" exists', async () => {
await addExceptionList({
http: mockKibanaHttpService(),
list: mockExceptionList,
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body:
'{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
method: 'PUT',
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await addExceptionList({
http: mockKibanaHttpService(),
list: mockNewExceptionList,
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(mockExceptionList);
});
});
describe('addExceptionListItem', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(mockExceptionItem);
});
test('check parameter url, body', async () => {
await addExceptionListItem({
http: mockKibanaHttpService(),
listItem: mockNewExceptionItem,
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
body:
'{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"item_id":"endpoint_list_item","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"simple"}',
method: 'POST',
signal: abortCtrl.signal,
});
});
test('check parameter url, body when "listItem.id" exists', async () => {
await addExceptionListItem({
http: mockKibanaHttpService(),
listItem: mockExceptionItem,
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
body:
'{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
method: 'PUT',
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await addExceptionListItem({
http: mockKibanaHttpService(),
listItem: mockNewExceptionItem,
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(mockExceptionItem);
});
});
describe('fetchExceptionListById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(mockExceptionList);
});
test('check parameter url, body', async () => {
await fetchExceptionListById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
method: 'GET',
query: {
id: '1',
},
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await fetchExceptionListById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(mockExceptionList);
});
});
describe('fetchExceptionListItemsByListId', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue([mockNewExceptionItem]);
});
test('check parameter url, body', async () => {
await fetchExceptionListItemsByListId({
http: mockKibanaHttpService(),
listId: 'endpoint_list',
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
method: 'GET',
query: {
list_id: 'endpoint_list',
},
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await fetchExceptionListItemsByListId({
http: mockKibanaHttpService(),
listId: 'endpoint_list',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual([mockNewExceptionItem]);
});
});
describe('fetchExceptionListItemById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue([mockNewExceptionItem]);
});
test('check parameter url, body', async () => {
await fetchExceptionListItemById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
method: 'GET',
query: {
id: '1',
},
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await fetchExceptionListItemById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual([mockNewExceptionItem]);
});
});
describe('deleteExceptionListById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(mockExceptionList);
});
test('check parameter url, body when deleting exception item', async () => {
await deleteExceptionListById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
method: 'DELETE',
query: {
id: '1',
},
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await deleteExceptionListById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(mockExceptionList);
});
});
describe('deleteExceptionListItemById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(mockExceptionItem);
});
test('check parameter url, body when deleting exception item', async () => {
await deleteExceptionListItemById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
method: 'DELETE',
query: {
id: '1',
},
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
const exceptionResponse = await deleteExceptionListItemById({
http: mockKibanaHttpService(),
id: '1',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(mockExceptionItem);
});
});
});

View file

@ -0,0 +1,158 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../common/constants';
import {
ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
} from '../../common/schemas';
import {
AddExceptionListItemProps,
AddExceptionListProps,
ApiCallByIdProps,
ApiCallByListIdProps,
} from './types';
/**
* Add provided ExceptionList
*
* @param list exception list to add
* @param signal to cancel request
*
* @throws An error if response is not OK
*
* Uses type assertion (list as ExceptionListSchema)
* per suggestion in Typescript union docs
*/
export const addExceptionList = async ({
http,
list,
signal,
}: AddExceptionListProps): Promise<ExceptionListSchema> =>
http.fetch<ExceptionListSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(list),
method: (list as ExceptionListSchema).id != null ? 'PUT' : 'POST',
signal,
});
/**
* Add provided ExceptionListItem
*
* @param listItem exception list item to add
* @param signal to cancel request
*
* @throws An error if response is not OK
*
* Uses type assertion (listItem as ExceptionListItemSchema)
* per suggestion in Typescript union docs
*/
export const addExceptionListItem = async ({
http,
listItem,
signal,
}: AddExceptionListItemProps): Promise<ExceptionListItemSchema> =>
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
body: JSON.stringify(listItem),
method: (listItem as ExceptionListItemSchema).id != null ? 'PUT' : 'POST',
signal,
});
/**
* Fetch an ExceptionList by providing a ExceptionList ID
*
* @param id ExceptionList ID (not list_id)
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const fetchExceptionListById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> =>
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}`, {
method: 'GET',
query: { id },
signal,
});
/**
* Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id
*
* @param id ExceptionList list_id (not ID)
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const fetchExceptionListItemsByListId = async ({
http,
listId,
signal,
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> =>
http.fetch<FoundExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}/_find`, {
method: 'GET',
query: { list_id: listId },
signal,
});
/**
* Fetch an ExceptionListItem by providing a ExceptionListItem ID
*
* @param id ExceptionListItem ID (not item_id)
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const fetchExceptionListItemById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
method: 'GET',
query: { id },
signal,
});
/**
* Delete an ExceptionList by providing a ExceptionList ID
*
* @param id ExceptionList ID (not list_id)
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const deleteExceptionListById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> =>
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}`, {
method: 'DELETE',
query: { id },
signal,
});
/**
* Delete an ExceptionListItem by providing a ExceptionListItem ID
*
* @param id ExceptionListItem ID (not item_id)
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const deleteExceptionListItemById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
method: 'DELETE',
query: { id },
signal,
});

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { mockExceptionItem } from '../mock';
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('usePersistExceptionItem', () => {
test('init', async () => {
const onError = jest.fn();
const { result } = renderHook<unknown, ReturnPersistExceptionItem>(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
});
test('saving exception item with isLoading === true', async () => {
await act(async () => {
const onError = jest.fn();
const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionItem>(
() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
result.current[1](mockExceptionItem);
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
});
});
test('saved exception item with isSaved === true', async () => {
const onError = jest.fn();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionItem>(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
result.current[1](mockExceptionItem);
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
});
});

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Dispatch, useEffect, useState } from 'react';
import { addExceptionListItem as persistExceptionItem } from '../api';
import { AddExceptionListItem, PersistHookProps } from '../types';
interface PersistReturnExceptionItem {
isLoading: boolean;
isSaved: boolean;
}
export type ReturnPersistExceptionItem = [
PersistReturnExceptionItem,
Dispatch<AddExceptionListItem | null>
];
export const usePersistExceptionItem = ({
http,
onError,
}: PersistHookProps): ReturnPersistExceptionItem => {
const [exceptionListItem, setExceptionItem] = useState<AddExceptionListItem | null>(null);
const [isSaved, setIsSaved] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setIsSaved(false);
const saveExceptionItem = async (): Promise<void> => {
if (exceptionListItem != null) {
try {
setIsLoading(true);
await persistExceptionItem({
http,
listItem: exceptionListItem,
signal: abortCtrl.signal,
});
if (isSubscribed) {
setIsSaved(true);
}
} catch (error) {
if (isSubscribed) {
onError(error);
}
}
if (isSubscribed) {
setIsLoading(false);
}
}
};
saveExceptionItem();
return (): void => {
isSubscribed = false;
abortCtrl.abort();
};
}, [http, exceptionListItem, onError]);
return [{ isLoading, isSaved }, setExceptionItem];
};

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { mockExceptionList } from '../mock';
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('usePersistExceptionList', () => {
test('init', async () => {
const onError = jest.fn();
const { result } = renderHook<unknown, ReturnPersistExceptionList>(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
});
test('saving exception list with isLoading === true', async () => {
const onError = jest.fn();
await act(async () => {
const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionList>(
() => usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
result.current[1](mockExceptionList);
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
});
});
test('saved exception list with isSaved === true', async () => {
const onError = jest.fn();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionList>(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
result.current[1](mockExceptionList);
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
});
});

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Dispatch, useEffect, useState } from 'react';
import { addExceptionList as persistExceptionList } from '../api';
import { AddExceptionList, PersistHookProps } from '../types';
interface PersistReturnExceptionList {
isLoading: boolean;
isSaved: boolean;
}
export type ReturnPersistExceptionList = [
PersistReturnExceptionList,
Dispatch<AddExceptionList | null>
];
export const usePersistExceptionList = ({
http,
onError,
}: PersistHookProps): ReturnPersistExceptionList => {
const [exceptionList, setExceptionList] = useState<AddExceptionList | null>(null);
const [isSaved, setIsSaved] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setIsSaved(false);
const saveExceptionList = async (): Promise<void> => {
if (exceptionList != null) {
try {
setIsLoading(true);
await persistExceptionList({ http, list: exceptionList, signal: abortCtrl.signal });
if (isSubscribed) {
setIsSaved(true);
}
} catch (error) {
if (isSubscribed) {
onError(error);
}
}
if (isSubscribed) {
setIsLoading(false);
}
}
};
saveExceptionList();
return (): void => {
isSubscribed = false;
abortCtrl.abort();
};
}, [http, exceptionList, onError]);
return [{ isLoading, isSaved }, setExceptionList];
};

View file

@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import * as api from '../api';
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('useExceptionList', () => {
test('init', async () => {
const onError = jest.fn();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnExceptionListAndItems>(() =>
useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError })
);
await waitForNextUpdate();
expect(result.current).toEqual([true, null]);
});
});
test('fetch exception list and items', async () => {
const onError = jest.fn();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnExceptionListAndItems>(() =>
useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError })
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([
false,
{
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
description: 'This is a sample endpoint type exception',
exceptionItems: {
data: [
{
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
comment: [],
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
description: 'This is a sample endpoint type exception',
entries: [
{
field: 'actingProcess.file.signer',
match: 'Elastic, N.V.',
match_any: undefined,
operator: 'included',
},
{
field: 'event.category',
match: undefined,
match_any: ['process', 'malware'],
operator: 'included',
},
],
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'simple',
updated_at: '2020-04-23T00:19:13.289Z',
updated_by: 'user_name',
},
],
page: 1,
per_page: 20,
total: 1,
},
id: '1',
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'endpoint',
updated_at: '2020-04-23T00:19:13.289Z',
updated_by: 'user_name',
},
]);
});
});
test('fetch a new exception list and its items', async () => {
const onError = jest.fn();
const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById');
const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId');
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, ReturnExceptionListAndItems>(
(id) => useExceptionList({ http: mockKibanaHttpService, id, onError }),
{
initialProps: 'myListId',
}
);
await waitForNextUpdate();
await waitForNextUpdate();
rerender('newListId');
await waitForNextUpdate();
expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2);
expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api';
import { ExceptionListAndItems, UseExceptionListProps } from '../types';
export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null];
/**
* Hook for using to get an ExceptionList and it's ExceptionListItems
*
* @param id desired ExceptionList ID (not list_id)
*
*/
export const useExceptionList = ({
http,
id,
onError,
}: UseExceptionListProps): ReturnExceptionListAndItems => {
const [exceptionListAndItems, setExceptionList] = useState<ExceptionListAndItems | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
const fetchData = async (idToFetch: string): Promise<void> => {
try {
setLoading(true);
const exceptionList = await fetchExceptionListById({
http,
id: idToFetch,
signal: abortCtrl.signal,
});
const exceptionListItems = await fetchExceptionListItemsByListId({
http,
listId: exceptionList.list_id,
signal: abortCtrl.signal,
});
if (isSubscribed) {
setExceptionList({ ...exceptionList, exceptionItems: { ...exceptionListItems } });
}
} catch (error) {
if (isSubscribed) {
setExceptionList(null);
onError(error);
}
}
if (isSubscribed) {
setLoading(false);
}
};
if (id != null) {
fetchData(id);
}
return (): void => {
isSubscribed = false;
abortCtrl.abort();
};
}, [http, id, onError]);
return [loading, exceptionListAndItems];
};

View file

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
CreateExceptionListItemSchemaPartial,
CreateExceptionListSchemaPartial,
ExceptionListItemSchema,
ExceptionListSchema,
} from '../../common/schemas';
export const mockExceptionList: ExceptionListSchema = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
description: 'This is a sample endpoint type exception',
id: '1',
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'endpoint',
updated_at: '2020-04-23T00:19:13.289Z',
updated_by: 'user_name',
};
export const mockNewExceptionList: CreateExceptionListSchemaPartial = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
description: 'This is a sample endpoint type exception',
list_id: 'endpoint_list',
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
type: 'endpoint',
};
export const mockNewExceptionItem: CreateExceptionListItemSchemaPartial = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
description: 'This is a sample endpoint type exception',
entries: [
{
field: 'actingProcess.file.signer',
match: 'Elastic, N.V.',
match_any: undefined,
operator: 'included',
},
{
field: 'event.category',
match: undefined,
match_any: ['process', 'malware'],
operator: 'included',
},
],
item_id: 'endpoint_list_item',
list_id: 'endpoint_list',
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
type: 'simple',
};
export const mockExceptionItem: ExceptionListItemSchema = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
comment: [],
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
description: 'This is a sample endpoint type exception',
entries: [
{
field: 'actingProcess.file.signer',
match: 'Elastic, N.V.',
match_any: undefined,
operator: 'included',
},
{
field: 'event.category',
match: undefined,
match_any: ['process', 'malware'],
operator: 'included',
},
],
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'simple',
updated_at: '2020-04-23T00:19:13.289Z',
updated_by: 'user_name',
};

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
CreateExceptionListItemSchemaPartial,
CreateExceptionListSchemaPartial,
ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
} from '../../common/schemas';
import { HttpStart } from '../../../../../src/core/public';
export interface ExceptionListAndItems extends ExceptionListSchema {
exceptionItems: FoundExceptionListItemSchema;
}
export type AddExceptionList = ExceptionListSchema | CreateExceptionListSchemaPartial;
export type AddExceptionListItem = CreateExceptionListItemSchemaPartial | ExceptionListItemSchema;
export interface PersistHookProps {
http: HttpStart;
onError: (arg: Error) => void;
}
export interface UseExceptionListProps {
http: HttpStart;
id: string | undefined;
onError: (arg: Error) => void;
}
export interface ApiCallByListIdProps {
http: HttpStart;
listId: string;
signal: AbortSignal;
}
export interface ApiCallByIdProps {
http: HttpStart;
id: string;
signal: AbortSignal;
}
export interface AddExceptionListProps {
http: HttpStart;
list: AddExceptionList;
signal: AbortSignal;
}
export interface AddExceptionListItemProps {
http: HttpStart;
listItem: AddExceptionListItem;
signal: AbortSignal;
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// Exports to be shared with plugins
export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list';
export { useExceptionList } from './exceptions/hooks/use_exception_list';
export {
mockExceptionItem,
mockExceptionList,
mockNewExceptionItem,
mockNewExceptionList,
} from './exceptions/mock';

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
useExceptionList,
usePersistExceptionItem,
usePersistExceptionList,
mockExceptionItem,
mockExceptionList,
mockNewExceptionItem,
mockNewExceptionList,
} from '../../lists/public';