Skip to content

Commit a0ec8ea

Browse files
peluja1012angorayc
authored andcommitted
[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
1 parent 4e338de commit a0ec8ea

12 files changed

Lines changed: 207 additions & 22 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { left } from 'fp-ts/lib/Either';
8+
import { pipe } from 'fp-ts/lib/pipeable';
9+
10+
import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
11+
12+
import { getExceptionListSchemaMock } from './exception_list_schema.mock';
13+
import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema';
14+
15+
describe('create_endpoint_list_schema', () => {
16+
test('it should validate a typical endpoint list response', () => {
17+
const payload = getExceptionListSchemaMock();
18+
const decoded = createEndpointListSchema.decode(payload);
19+
const checked = exactCheck(payload, decoded);
20+
const message = pipe(checked, foldLeftRight);
21+
22+
expect(getPaths(left(message.errors))).toEqual([]);
23+
expect(message.schema).toEqual(payload);
24+
});
25+
26+
test('it should accept an empty object when an endpoint list already exists', () => {
27+
const payload = {};
28+
const decoded = createEndpointListSchema.decode(payload);
29+
const checked = exactCheck(payload, decoded);
30+
const message = pipe(checked, foldLeftRight);
31+
32+
expect(getPaths(left(message.errors))).toEqual([]);
33+
expect(message.schema).toEqual(payload);
34+
});
35+
36+
test('it should NOT allow missing fields', () => {
37+
const payload = getExceptionListSchemaMock();
38+
delete payload.list_id;
39+
const decoded = createEndpointListSchema.decode(payload);
40+
const checked = exactCheck(payload, decoded);
41+
const message = pipe(checked, foldLeftRight);
42+
43+
expect(getPaths(left(message.errors)).length).toEqual(1);
44+
expect(message.schema).toEqual({});
45+
});
46+
47+
test('it should not allow an extra key to be sent in', () => {
48+
const payload: CreateEndpointListSchema & {
49+
extraKey?: string;
50+
} = getExceptionListSchemaMock();
51+
payload.extraKey = 'some new value';
52+
const decoded = createEndpointListSchema.decode(payload);
53+
const checked = exactCheck(payload, decoded);
54+
const message = pipe(checked, foldLeftRight);
55+
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
56+
expect(message.schema).toEqual({});
57+
});
58+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
/* eslint-disable @typescript-eslint/camelcase */
8+
9+
import * as t from 'io-ts';
10+
11+
import { exceptionListSchema } from './exception_list_schema';
12+
13+
export const createEndpointListSchema = t.union([exceptionListSchema, t.exact(t.type({}))]);
14+
15+
export type CreateEndpointListSchema = t.TypeOf<typeof createEndpointListSchema>;

x-pack/plugins/lists/common/schemas/response/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
export * from './acknowledge_schema';
8+
export * from './create_endpoint_list_schema';
89
export * from './exception_list_schema';
910
export * from './exception_list_item_schema';
1011
export * from './found_exception_list_item_schema';

x-pack/plugins/lists/common/shared_exports.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {
1212
CreateComments,
1313
ExceptionListSchema,
1414
ExceptionListItemSchema,
15+
CreateExceptionListSchema,
1516
CreateExceptionListItemSchema,
1617
UpdateExceptionListItemSchema,
1718
Entry,
@@ -41,3 +42,5 @@ export {
4142
ExceptionListType,
4243
Type,
4344
} from './schemas';
45+
46+
export { ENDPOINT_LIST_ID } from './constants';

x-pack/plugins/lists/public/exceptions/api.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '../../common/schemas';
2020

2121
import {
22+
addEndpointExceptionList,
2223
addExceptionList,
2324
addExceptionListItem,
2425
deleteExceptionListById,
@@ -738,4 +739,39 @@ describe('Exceptions Lists API', () => {
738739
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
739740
});
740741
});
742+
743+
describe('#addEndpointExceptionList', () => {
744+
beforeEach(() => {
745+
fetchMock.mockClear();
746+
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
747+
});
748+
749+
test('it invokes "addEndpointExceptionList" with expected url and body values', async () => {
750+
await addEndpointExceptionList({
751+
http: mockKibanaHttpService(),
752+
signal: abortCtrl.signal,
753+
});
754+
expect(fetchMock).toHaveBeenCalledWith('/api/endpoint_list', {
755+
method: 'POST',
756+
signal: abortCtrl.signal,
757+
});
758+
});
759+
760+
test('it returns expected exception list on success', async () => {
761+
const exceptionResponse = await addEndpointExceptionList({
762+
http: mockKibanaHttpService(),
763+
signal: abortCtrl.signal,
764+
});
765+
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
766+
});
767+
768+
test('it returns an empty object when list already exists', async () => {
769+
fetchMock.mockResolvedValue({});
770+
const exceptionResponse = await addEndpointExceptionList({
771+
http: mockKibanaHttpService(),
772+
signal: abortCtrl.signal,
773+
});
774+
expect(exceptionResponse).toEqual({});
775+
});
776+
});
741777
});

x-pack/plugins/lists/public/exceptions/api.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66
import {
7+
ENDPOINT_LIST_URL,
78
EXCEPTION_LIST_ITEM_URL,
89
EXCEPTION_LIST_NAMESPACE,
910
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
1011
EXCEPTION_LIST_URL,
1112
} from '../../common/constants';
1213
import {
14+
CreateEndpointListSchema,
1315
ExceptionListItemSchema,
1416
ExceptionListSchema,
1517
FoundExceptionListItemSchema,
18+
createEndpointListSchema,
1619
createExceptionListItemSchema,
1720
createExceptionListSchema,
1821
deleteExceptionListItemSchema,
@@ -29,6 +32,7 @@ import {
2932
import { validate } from '../../common/siem_common_deps';
3033

3134
import {
35+
AddEndpointExceptionListProps,
3236
AddExceptionListItemProps,
3337
AddExceptionListProps,
3438
ApiCallByIdProps,
@@ -440,3 +444,34 @@ export const deleteExceptionListItemById = async ({
440444
return Promise.reject(errorsRequest);
441445
}
442446
};
447+
448+
/**
449+
* Add new Endpoint ExceptionList
450+
*
451+
* @param http Kibana http service
452+
* @param signal to cancel request
453+
*
454+
* @throws An error if response is not OK
455+
*
456+
*/
457+
export const addEndpointExceptionList = async ({
458+
http,
459+
signal,
460+
}: AddEndpointExceptionListProps): Promise<CreateEndpointListSchema> => {
461+
try {
462+
const response = await http.fetch<ExceptionListItemSchema>(ENDPOINT_LIST_URL, {
463+
method: 'POST',
464+
signal,
465+
});
466+
467+
const [validatedResponse, errorsResponse] = validate(response, createEndpointListSchema);
468+
469+
if (errorsResponse != null || validatedResponse == null) {
470+
return Promise.reject(errorsResponse);
471+
} else {
472+
return Promise.resolve(validatedResponse);
473+
}
474+
} catch (error) {
475+
return Promise.reject(error);
476+
}
477+
};

x-pack/plugins/lists/public/exceptions/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,8 @@ export interface UpdateExceptionListItemProps {
110110
listItem: UpdateExceptionListItemSchema;
111111
signal: AbortSignal;
112112
}
113+
114+
export interface AddEndpointExceptionListProps {
115+
http: HttpStart;
116+
signal: AbortSignal;
117+
}

x-pack/plugins/lists/public/shared_exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export {
2424
updateExceptionListItem,
2525
fetchExceptionListById,
2626
addExceptionList,
27+
addEndpointExceptionList,
2728
} from './exceptions/api';
2829
export {
2930
ExceptionList,

x-pack/plugins/security_solution/common/shared_imports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {
1212
CreateComments,
1313
ExceptionListSchema,
1414
ExceptionListItemSchema,
15+
CreateExceptionListSchema,
1516
CreateExceptionListItemSchema,
1617
UpdateExceptionListItemSchema,
1718
Entry,
@@ -40,4 +41,5 @@ export {
4041
namespaceType,
4142
ExceptionListType,
4243
Type,
44+
ENDPOINT_LIST_ID,
4345
} from '../../lists/common';

x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ describe('useFetchOrCreateRuleExceptionList', () => {
2727
let fetchRuleById: jest.SpyInstance<ReturnType<typeof rulesApi.fetchRuleById>>;
2828
let patchRule: jest.SpyInstance<ReturnType<typeof rulesApi.patchRule>>;
2929
let addExceptionList: jest.SpyInstance<ReturnType<typeof listsApi.addExceptionList>>;
30+
let addEndpointExceptionList: jest.SpyInstance<ReturnType<
31+
typeof listsApi.addEndpointExceptionList
32+
>>;
3033
let fetchExceptionListById: jest.SpyInstance<ReturnType<typeof listsApi.fetchExceptionListById>>;
3134
let render: (
3235
listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType']
@@ -75,6 +78,10 @@ describe('useFetchOrCreateRuleExceptionList', () => {
7578
.spyOn(listsApi, 'addExceptionList')
7679
.mockResolvedValue(newDetectionExceptionList);
7780

81+
addEndpointExceptionList = jest
82+
.spyOn(listsApi, 'addEndpointExceptionList')
83+
.mockResolvedValue(newEndpointExceptionList);
84+
7885
fetchExceptionListById = jest
7986
.spyOn(listsApi, 'fetchExceptionListById')
8087
.mockResolvedValue(detectionExceptionList);
@@ -299,7 +306,7 @@ describe('useFetchOrCreateRuleExceptionList', () => {
299306
await waitForNextUpdate();
300307
await waitForNextUpdate();
301308
await waitForNextUpdate();
302-
expect(addExceptionList).toHaveBeenCalledTimes(1);
309+
expect(addEndpointExceptionList).toHaveBeenCalledTimes(1);
303310
});
304311
});
305312
it('should update the rule', async () => {

0 commit comments

Comments
 (0)