mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
c9c37f77e6
commit
3c48b3acd0
14 changed files with 1104 additions and 0 deletions
12
x-pack/plugins/lists/public/common/mocks/kibana_core.ts
Normal file
12
x-pack/plugins/lists/public/common/mocks/kibana_core.ts
Normal 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();
|
51
x-pack/plugins/lists/public/exceptions/__mocks__/api.ts
Normal file
51
x-pack/plugins/lists/public/exceptions/__mocks__/api.ts
Normal 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);
|
285
x-pack/plugins/lists/public/exceptions/api.test.ts
Normal file
285
x-pack/plugins/lists/public/exceptions/api.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
158
x-pack/plugins/lists/public/exceptions/api.ts
Normal file
158
x-pack/plugins/lists/public/exceptions/api.ts
Normal 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,
|
||||
});
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
92
x-pack/plugins/lists/public/exceptions/mock.ts
Normal file
92
x-pack/plugins/lists/public/exceptions/mock.ts
Normal 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',
|
||||
};
|
57
x-pack/plugins/lists/public/exceptions/types.ts
Normal file
57
x-pack/plugins/lists/public/exceptions/types.ts
Normal 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;
|
||||
}
|
15
x-pack/plugins/lists/public/index.tsx
Normal file
15
x-pack/plugins/lists/public/index.tsx
Normal 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';
|
15
x-pack/plugins/siem/public/lists_plugin_deps.ts
Normal file
15
x-pack/plugins/siem/public/lists_plugin_deps.ts
Normal 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';
|
Loading…
Add table
Add a link
Reference in a new issue