[Security Solution][Exception Modal] Create endpoint exception list if it doesn't already exist (#71807)
* use createEndpointList api * fix lint * update list id constant * add schema test * add api test
This commit is contained in:
parent
a282af7ca3
commit
e4f7acb90f
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { getExceptionListSchemaMock } from './exception_list_schema.mock';
|
||||
import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema';
|
||||
|
||||
describe('create_endpoint_list_schema', () => {
|
||||
test('it should validate a typical endpoint list response', () => {
|
||||
const payload = getExceptionListSchemaMock();
|
||||
const decoded = createEndpointListSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should accept an empty object when an endpoint list already exists', () => {
|
||||
const payload = {};
|
||||
const decoded = createEndpointListSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT allow missing fields', () => {
|
||||
const payload = getExceptionListSchemaMock();
|
||||
delete payload.list_id;
|
||||
const decoded = createEndpointListSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors)).length).toEqual(1);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not allow an extra key to be sent in', () => {
|
||||
const payload: CreateEndpointListSchema & {
|
||||
extraKey?: string;
|
||||
} = getExceptionListSchemaMock();
|
||||
payload.extraKey = 'some new value';
|
||||
const decoded = createEndpointListSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { exceptionListSchema } from './exception_list_schema';
|
||||
|
||||
export const createEndpointListSchema = t.union([exceptionListSchema, t.exact(t.type({}))]);
|
||||
|
||||
export type CreateEndpointListSchema = t.TypeOf<typeof createEndpointListSchema>;
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
export * from './acknowledge_schema';
|
||||
export * from './create_endpoint_list_schema';
|
||||
export * from './exception_list_schema';
|
||||
export * from './exception_list_item_schema';
|
||||
export * from './found_exception_list_item_schema';
|
||||
|
|
|
@ -12,6 +12,7 @@ export {
|
|||
CreateComments,
|
||||
ExceptionListSchema,
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
Entry,
|
||||
|
@ -41,3 +42,5 @@ export {
|
|||
ExceptionListType,
|
||||
Type,
|
||||
} from './schemas';
|
||||
|
||||
export { ENDPOINT_LIST_ID } from './constants';
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../common/schemas';
|
||||
|
||||
import {
|
||||
addEndpointExceptionList,
|
||||
addExceptionList,
|
||||
addExceptionListItem,
|
||||
deleteExceptionListById,
|
||||
|
@ -738,4 +739,39 @@ describe('Exceptions Lists API', () => {
|
|||
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addEndpointExceptionList', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
test('it invokes "addEndpointExceptionList" with expected url and body values', async () => {
|
||||
await addEndpointExceptionList({
|
||||
http: mockKibanaHttpService(),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/endpoint_list', {
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns expected exception list on success', async () => {
|
||||
const exceptionResponse = await addEndpointExceptionList({
|
||||
http: mockKibanaHttpService(),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
test('it returns an empty object when list already exists', async () => {
|
||||
fetchMock.mockResolvedValue({});
|
||||
const exceptionResponse = await addEndpointExceptionList({
|
||||
http: mockKibanaHttpService(),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,15 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
ENDPOINT_LIST_URL,
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
EXCEPTION_LIST_NAMESPACE,
|
||||
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
|
||||
EXCEPTION_LIST_URL,
|
||||
} from '../../common/constants';
|
||||
import {
|
||||
CreateEndpointListSchema,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
createEndpointListSchema,
|
||||
createExceptionListItemSchema,
|
||||
createExceptionListSchema,
|
||||
deleteExceptionListItemSchema,
|
||||
|
@ -29,6 +32,7 @@ import {
|
|||
import { validate } from '../../common/siem_common_deps';
|
||||
|
||||
import {
|
||||
AddEndpointExceptionListProps,
|
||||
AddExceptionListItemProps,
|
||||
AddExceptionListProps,
|
||||
ApiCallByIdProps,
|
||||
|
@ -440,3 +444,34 @@ export const deleteExceptionListItemById = async ({
|
|||
return Promise.reject(errorsRequest);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new Endpoint ExceptionList
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*
|
||||
*/
|
||||
export const addEndpointExceptionList = async ({
|
||||
http,
|
||||
signal,
|
||||
}: AddEndpointExceptionListProps): Promise<CreateEndpointListSchema> => {
|
||||
try {
|
||||
const response = await http.fetch<ExceptionListItemSchema>(ENDPOINT_LIST_URL, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
});
|
||||
|
||||
const [validatedResponse, errorsResponse] = validate(response, createEndpointListSchema);
|
||||
|
||||
if (errorsResponse != null || validatedResponse == null) {
|
||||
return Promise.reject(errorsResponse);
|
||||
} else {
|
||||
return Promise.resolve(validatedResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -110,3 +110,8 @@ export interface UpdateExceptionListItemProps {
|
|||
listItem: UpdateExceptionListItemSchema;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface AddEndpointExceptionListProps {
|
||||
http: HttpStart;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export {
|
|||
updateExceptionListItem,
|
||||
fetchExceptionListById,
|
||||
addExceptionList,
|
||||
addEndpointExceptionList,
|
||||
} from './exceptions/api';
|
||||
export {
|
||||
ExceptionList,
|
||||
|
|
|
@ -12,6 +12,7 @@ export {
|
|||
CreateComments,
|
||||
ExceptionListSchema,
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
Entry,
|
||||
|
@ -40,4 +41,5 @@ export {
|
|||
namespaceType,
|
||||
ExceptionListType,
|
||||
Type,
|
||||
ENDPOINT_LIST_ID,
|
||||
} from '../../lists/common';
|
||||
|
|
|
@ -27,6 +27,9 @@ describe('useFetchOrCreateRuleExceptionList', () => {
|
|||
let fetchRuleById: jest.SpyInstance<ReturnType<typeof rulesApi.fetchRuleById>>;
|
||||
let patchRule: jest.SpyInstance<ReturnType<typeof rulesApi.patchRule>>;
|
||||
let addExceptionList: jest.SpyInstance<ReturnType<typeof listsApi.addExceptionList>>;
|
||||
let addEndpointExceptionList: jest.SpyInstance<ReturnType<
|
||||
typeof listsApi.addEndpointExceptionList
|
||||
>>;
|
||||
let fetchExceptionListById: jest.SpyInstance<ReturnType<typeof listsApi.fetchExceptionListById>>;
|
||||
let render: (
|
||||
listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType']
|
||||
|
@ -75,6 +78,10 @@ describe('useFetchOrCreateRuleExceptionList', () => {
|
|||
.spyOn(listsApi, 'addExceptionList')
|
||||
.mockResolvedValue(newDetectionExceptionList);
|
||||
|
||||
addEndpointExceptionList = jest
|
||||
.spyOn(listsApi, 'addEndpointExceptionList')
|
||||
.mockResolvedValue(newEndpointExceptionList);
|
||||
|
||||
fetchExceptionListById = jest
|
||||
.spyOn(listsApi, 'fetchExceptionListById')
|
||||
.mockResolvedValue(detectionExceptionList);
|
||||
|
@ -299,7 +306,7 @@ describe('useFetchOrCreateRuleExceptionList', () => {
|
|||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(addExceptionList).toHaveBeenCalledTimes(1);
|
||||
expect(addEndpointExceptionList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
it('should update the rule', async () => {
|
||||
|
|
|
@ -7,17 +7,22 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { HttpStart } from '../../../../../../../src/core/public';
|
||||
|
||||
import {
|
||||
ExceptionListSchema,
|
||||
CreateExceptionListSchema,
|
||||
} from '../../../../../lists/common/schemas';
|
||||
import { Rule } from '../../../detections/containers/detection_engine/rules/types';
|
||||
import { List, ListArray } from '../../../../common/detection_engine/schemas/types';
|
||||
import {
|
||||
fetchRuleById,
|
||||
patchRule,
|
||||
} from '../../../detections/containers/detection_engine/rules/api';
|
||||
import { fetchExceptionListById, addExceptionList } from '../../../lists_plugin_deps';
|
||||
import {
|
||||
fetchExceptionListById,
|
||||
addExceptionList,
|
||||
addEndpointExceptionList,
|
||||
} from '../../../lists_plugin_deps';
|
||||
import {
|
||||
ExceptionListSchema,
|
||||
CreateExceptionListSchema,
|
||||
ENDPOINT_LIST_ID,
|
||||
} from '../../../../common/shared_imports';
|
||||
|
||||
export type ReturnUseFetchOrCreateRuleExceptionList = [boolean, ExceptionListSchema | null];
|
||||
|
||||
|
@ -51,27 +56,43 @@ export const useFetchOrCreateRuleExceptionList = ({
|
|||
const abortCtrl = new AbortController();
|
||||
|
||||
async function createExceptionList(ruleResponse: Rule): Promise<ExceptionListSchema> {
|
||||
const exceptionListToCreate: CreateExceptionListSchema = {
|
||||
name: ruleResponse.name,
|
||||
description: ruleResponse.description,
|
||||
type: exceptionListType,
|
||||
namespace_type: exceptionListType === 'endpoint' ? 'agnostic' : 'single',
|
||||
_tags: undefined,
|
||||
tags: undefined,
|
||||
list_id: exceptionListType === 'endpoint' ? 'endpoint_list' : undefined,
|
||||
meta: undefined,
|
||||
};
|
||||
try {
|
||||
const newExceptionList = await addExceptionList({
|
||||
let newExceptionList: ExceptionListSchema;
|
||||
if (exceptionListType === 'endpoint') {
|
||||
const possibleEndpointExceptionList = await addEndpointExceptionList({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
if (Object.keys(possibleEndpointExceptionList).length === 0) {
|
||||
// Endpoint exception list already exists, fetch it
|
||||
newExceptionList = await fetchExceptionListById({
|
||||
http,
|
||||
id: ENDPOINT_LIST_ID,
|
||||
namespaceType: 'agnostic',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
} else {
|
||||
newExceptionList = possibleEndpointExceptionList as ExceptionListSchema;
|
||||
}
|
||||
} else {
|
||||
const exceptionListToCreate: CreateExceptionListSchema = {
|
||||
name: ruleResponse.name,
|
||||
description: ruleResponse.description,
|
||||
type: exceptionListType,
|
||||
namespace_type: 'single',
|
||||
list_id: undefined,
|
||||
_tags: undefined,
|
||||
tags: undefined,
|
||||
meta: undefined,
|
||||
};
|
||||
newExceptionList = await addExceptionList({
|
||||
http,
|
||||
list: exceptionListToCreate,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
return Promise.resolve(newExceptionList);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return Promise.resolve(newExceptionList);
|
||||
}
|
||||
|
||||
async function createAndAssociateExceptionList(
|
||||
ruleResponse: Rule
|
||||
): Promise<ExceptionListSchema> {
|
||||
|
@ -133,7 +154,7 @@ export const useFetchOrCreateRuleExceptionList = ({
|
|||
let exceptionListToUse: ExceptionListSchema;
|
||||
const matchingList = exceptionLists.find((list) => {
|
||||
if (exceptionListType === 'endpoint') {
|
||||
return list.type === exceptionListType && list.list_id === 'endpoint_list';
|
||||
return list.type === exceptionListType && list.list_id === ENDPOINT_LIST_ID;
|
||||
} else {
|
||||
return list.type === exceptionListType;
|
||||
}
|
||||
|
|
|
@ -49,4 +49,5 @@ export {
|
|||
ExceptionList,
|
||||
Pagination,
|
||||
UseExceptionListSuccess,
|
||||
addEndpointExceptionList,
|
||||
} from '../../lists/public';
|
||||
|
|
Loading…
Reference in a new issue