mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Endpoint] New route for create an exception list and return the existing one if it already exists (#139618)
* new route for create an exception list and return the existing one if alredy exists * Fixes unit test and shows error when ignore_existing set to false and there is a conflict * Remove query param and update route name to be more specific * Fixes unit test * Enforce list_id and type types for internal route. Added unit tests * Uses existing constants to define list_ids * Don't create host isolation exeptions api client if not needed when checking links availability Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a6a2706b8d
commit
e459752466
13 changed files with 288 additions and 78 deletions
|
@ -41,3 +41,6 @@ export * from './update_exception_list_item_validation';
|
|||
export * from './update_exception_list_schema';
|
||||
export * from './update_list_item_schema';
|
||||
export * from './update_list_schema';
|
||||
|
||||
// Internal routes
|
||||
export * from './internal/create_exception_list_schema';
|
||||
|
|
|
@ -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
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { ExceptionListTypeEnum } from '../../../common/exception_list';
|
||||
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import { internalCreateExceptionListSchema } from '.';
|
||||
import { getCreateExceptionListSchemaMock } from '../../create_exception_list_schema/index.mock';
|
||||
|
||||
describe('create_exception_list_schema', () => {
|
||||
test('it should accept artifact list_id', () => {
|
||||
const payload = {
|
||||
...getCreateExceptionListSchemaMock(),
|
||||
list_id: ExceptionListTypeEnum.ENDPOINT_BLOCKLISTS,
|
||||
};
|
||||
const decoded = internalCreateExceptionListSchema.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 fail when invalid list_id', () => {
|
||||
const payload = {
|
||||
...getCreateExceptionListSchemaMock(),
|
||||
list_id: ExceptionListTypeEnum.DETECTION,
|
||||
};
|
||||
const decoded = internalCreateExceptionListSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "detection" supplied to "list_id"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
test('it should accept artifact type', () => {
|
||||
const payload = {
|
||||
...getCreateExceptionListSchemaMock(),
|
||||
list_id: ExceptionListTypeEnum.ENDPOINT_BLOCKLISTS,
|
||||
type: ExceptionListTypeEnum.ENDPOINT_BLOCKLISTS,
|
||||
};
|
||||
const decoded = internalCreateExceptionListSchema.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 fail when invalid type', () => {
|
||||
const payload = {
|
||||
...getCreateExceptionListSchemaMock(),
|
||||
list_id: ExceptionListTypeEnum.ENDPOINT_BLOCKLISTS,
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
};
|
||||
const decoded = internalCreateExceptionListSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "detection" supplied to "type"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
ENDPOINT_BLOCKLISTS_LIST_ID,
|
||||
ENDPOINT_EVENT_FILTERS_LIST_ID,
|
||||
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
|
||||
ENDPOINT_TRUSTED_APPS_LIST_ID,
|
||||
} from '@kbn/securitysolution-list-constants';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import {
|
||||
createExceptionListSchema,
|
||||
CreateExceptionListSchemaDecoded,
|
||||
} from '../../create_exception_list_schema';
|
||||
|
||||
export const internalCreateExceptionListSchema = t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
type: t.keyof({
|
||||
endpoint: null,
|
||||
endpoint_events: null,
|
||||
endpoint_host_isolation_exceptions: null,
|
||||
endpoint_blocklists: null,
|
||||
}),
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
// TODO: Move the ALL_ENDPOINT_ARTIFACT_LIST_IDS inside the package and use it here instead
|
||||
list_id: t.keyof({
|
||||
[ENDPOINT_TRUSTED_APPS_LIST_ID]: null,
|
||||
[ENDPOINT_EVENT_FILTERS_LIST_ID]: null,
|
||||
[ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID]: null,
|
||||
[ENDPOINT_BLOCKLISTS_LIST_ID]: null,
|
||||
}),
|
||||
})
|
||||
),
|
||||
createExceptionListSchema,
|
||||
]);
|
||||
|
||||
export type InternalCreateExceptionListSchema = t.OutputOf<
|
||||
typeof internalCreateExceptionListSchema
|
||||
>;
|
||||
|
||||
// This type is used after a decode since some things are defaults after a decode.
|
||||
export type InternalCreateExceptionListSchemaDecoded = CreateExceptionListSchemaDecoded;
|
|
@ -20,6 +20,12 @@ export const LIST_PRIVILEGES_URL = `${LIST_URL}/privileges`;
|
|||
export const EXCEPTION_LIST_URL = '/api/exception_lists';
|
||||
export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items';
|
||||
|
||||
/**
|
||||
* Internal exception list routes
|
||||
*/
|
||||
export const INTERNAL_EXCEPTION_LIST_URL = `/internal${EXCEPTION_LIST_URL}`;
|
||||
export const INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL = `${INTERNAL_EXCEPTION_LIST_URL}/_create`;
|
||||
|
||||
/**
|
||||
* Exception list spaces
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { validate } from '@kbn/securitysolution-io-ts-utils';
|
||||
import {
|
||||
CreateExceptionListSchemaDecoded,
|
||||
exceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { IKibanaResponse, KibanaRequest, KibanaResponseFactory } from '@kbn/core-http-server';
|
||||
|
||||
import { SiemResponseFactory, getExceptionListClient } from '../routes';
|
||||
import { ListsRequestHandlerContext } from '../types';
|
||||
|
||||
export const createExceptionListHandler = async (
|
||||
context: ListsRequestHandlerContext,
|
||||
request: KibanaRequest<unknown, unknown, CreateExceptionListSchemaDecoded, 'post'>,
|
||||
response: KibanaResponseFactory,
|
||||
siemResponse: SiemResponseFactory,
|
||||
options: { ignoreExisting: boolean } = { ignoreExisting: false }
|
||||
): Promise<IKibanaResponse> => {
|
||||
const {
|
||||
name,
|
||||
tags,
|
||||
meta,
|
||||
namespace_type: namespaceType,
|
||||
description,
|
||||
list_id: listId,
|
||||
type,
|
||||
version,
|
||||
} = request.body;
|
||||
const exceptionLists = await getExceptionListClient(context);
|
||||
const exceptionList = await exceptionLists.getExceptionList({
|
||||
id: undefined,
|
||||
listId,
|
||||
namespaceType,
|
||||
});
|
||||
|
||||
if (exceptionList != null) {
|
||||
if (options.ignoreExisting) {
|
||||
return response.ok({ body: exceptionList });
|
||||
}
|
||||
return siemResponse.error({
|
||||
body: `exception list id: "${listId}" already exists`,
|
||||
statusCode: 409,
|
||||
});
|
||||
} else {
|
||||
const createdList = await exceptionLists.createExceptionList({
|
||||
description,
|
||||
immutable: false,
|
||||
listId,
|
||||
meta,
|
||||
name,
|
||||
namespaceType,
|
||||
tags,
|
||||
type,
|
||||
version,
|
||||
});
|
||||
const [validated, errors] = validate(createdList, exceptionListSchema);
|
||||
if (errors != null) {
|
||||
return siemResponse.error({ body: errors, statusCode: 500 });
|
||||
} else {
|
||||
return response.ok({ body: validated ?? {} });
|
||||
}
|
||||
}
|
||||
};
|
|
@ -5,19 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { validate } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import {
|
||||
CreateExceptionListSchemaDecoded,
|
||||
createExceptionListSchema,
|
||||
exceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
|
||||
import type { ListsPluginRouter } from '../types';
|
||||
import { createExceptionListHandler } from '../handlers/create_exception_list_handler';
|
||||
|
||||
import { buildRouteValidation, buildSiemResponse } from './utils';
|
||||
import { getExceptionListClient } from './utils/get_exception_list_client';
|
||||
|
||||
export const createExceptionListRoute = (router: ListsPluginRouter): void => {
|
||||
router.post(
|
||||
|
@ -36,46 +34,7 @@ export const createExceptionListRoute = (router: ListsPluginRouter): void => {
|
|||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
tags,
|
||||
meta,
|
||||
namespace_type: namespaceType,
|
||||
description,
|
||||
list_id: listId,
|
||||
type,
|
||||
version,
|
||||
} = request.body;
|
||||
const exceptionLists = await getExceptionListClient(context);
|
||||
const exceptionList = await exceptionLists.getExceptionList({
|
||||
id: undefined,
|
||||
listId,
|
||||
namespaceType,
|
||||
});
|
||||
if (exceptionList != null) {
|
||||
return siemResponse.error({
|
||||
body: `exception list id: "${listId}" already exists`,
|
||||
statusCode: 409,
|
||||
});
|
||||
} else {
|
||||
const createdList = await exceptionLists.createExceptionList({
|
||||
description,
|
||||
immutable: false,
|
||||
listId,
|
||||
meta,
|
||||
name,
|
||||
namespaceType,
|
||||
tags,
|
||||
type,
|
||||
version,
|
||||
});
|
||||
const [validated, errors] = validate(createdList, exceptionListSchema);
|
||||
if (errors != null) {
|
||||
return siemResponse.error({ body: errors, statusCode: 500 });
|
||||
} else {
|
||||
return response.ok({ body: validated ?? {} });
|
||||
}
|
||||
}
|
||||
return await createExceptionListHandler(context, request, response, siemResponse);
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
|
|
|
@ -44,3 +44,6 @@ export * from './update_exception_list_route';
|
|||
export * from './update_list_item_route';
|
||||
export * from './update_list_route';
|
||||
export * from './utils';
|
||||
|
||||
// internal
|
||||
export * from './internal/create_exceptions_list_route';
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
findListRoute,
|
||||
importExceptionsRoute,
|
||||
importListItemRoute,
|
||||
internalCreateExceptionListRoute,
|
||||
patchListItemRoute,
|
||||
patchListRoute,
|
||||
readEndpointListItemRoute,
|
||||
|
@ -103,4 +104,7 @@ export const initRoutes = (router: ListsPluginRouter, config: ConfigType): void
|
|||
|
||||
// exception list items summary
|
||||
summaryExceptionListRoute(router);
|
||||
|
||||
// internal routes
|
||||
internalCreateExceptionListRoute(router);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import {
|
||||
InternalCreateExceptionListSchemaDecoded,
|
||||
internalCreateExceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL } from '@kbn/securitysolution-list-constants';
|
||||
|
||||
import { createExceptionListHandler } from '../../handlers/create_exception_list_handler';
|
||||
import type { ListsPluginRouter } from '../../types';
|
||||
import { buildRouteValidation, buildSiemResponse } from '../utils';
|
||||
|
||||
export const internalCreateExceptionListRoute = (router: ListsPluginRouter): void => {
|
||||
router.post(
|
||||
{
|
||||
options: {
|
||||
tags: ['access:lists-all'],
|
||||
},
|
||||
path: INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL,
|
||||
validate: {
|
||||
body: buildRouteValidation<
|
||||
typeof internalCreateExceptionListSchema,
|
||||
InternalCreateExceptionListSchemaDecoded
|
||||
>(internalCreateExceptionListSchema),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
try {
|
||||
return await createExceptionListHandler(context, request, response, siemResponse, {
|
||||
ignoreExisting: true,
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -56,7 +56,10 @@ describe('links', () => {
|
|||
|
||||
it('it returns all links without filtering when not having isolation permissions but has at least one host isolation exceptions entry', async () => {
|
||||
fakeHttpServices.get.mockResolvedValue({ total: 1 });
|
||||
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
|
||||
const filteredLinks = await getManagementFilteredLinks(
|
||||
coreMockStarted,
|
||||
getPlugins(['superuser'])
|
||||
);
|
||||
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
|
||||
expect(filteredLinks).toEqual(links);
|
||||
});
|
||||
|
|
|
@ -222,11 +222,13 @@ export const getManagementFilteredLinks = async (
|
|||
plugins.fleet?.authz,
|
||||
currentUserResponse.roles
|
||||
);
|
||||
const hostIsolationExceptionsApiClientInstance = HostIsolationExceptionsApiClient.getInstance(
|
||||
core.http
|
||||
);
|
||||
|
||||
if (!privileges.canAccessEndpointManagement) {
|
||||
return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
|
||||
}
|
||||
if (!privileges.canIsolateHost) {
|
||||
const hostIsolationExceptionsApiClientInstance = HostIsolationExceptionsApiClient.getInstance(
|
||||
core.http
|
||||
);
|
||||
const summaryResponse = await hostIsolationExceptionsApiClientInstance.summary();
|
||||
if (!summaryResponse.total) {
|
||||
return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
import type { CoreStart, HttpSetup } from '@kbn/core/public';
|
||||
import type { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
import {
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
EXCEPTION_LIST_URL,
|
||||
INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL,
|
||||
} from '@kbn/securitysolution-list-constants';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import { ExceptionsListApiClient } from './exceptions_list_api_client';
|
||||
|
@ -62,9 +66,12 @@ describe('Exceptions List Api Client', () => {
|
|||
const exceptionsListApiClientInstance = getInstance();
|
||||
|
||||
expect(fakeHttpServices.post).toHaveBeenCalledTimes(1);
|
||||
expect(fakeHttpServices.post).toHaveBeenCalledWith(EXCEPTION_LIST_URL, {
|
||||
body: JSON.stringify(getFakeListDefinition()),
|
||||
});
|
||||
expect(fakeHttpServices.post).toHaveBeenCalledWith(
|
||||
INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL,
|
||||
{
|
||||
body: JSON.stringify(getFakeListDefinition()),
|
||||
}
|
||||
);
|
||||
expect(exceptionsListApiClientInstance).toBeDefined();
|
||||
});
|
||||
|
||||
|
@ -117,22 +124,6 @@ describe('Exceptions List Api Client', () => {
|
|||
expect(err.response.status).toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
it('Creating an instance when list already exists does not throw', async () => {
|
||||
fakeHttpServices.post.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 409,
|
||||
},
|
||||
});
|
||||
const newFakeListId = 'fakeListIdV4';
|
||||
const notFailedInstance = new ExceptionsListApiClient(
|
||||
fakeHttpServices,
|
||||
newFakeListId,
|
||||
getFakeListDefinition()
|
||||
);
|
||||
await notFailedInstance.find(getQueryParams());
|
||||
expect(notFailedInstance).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wen using public methods', () => {
|
||||
|
|
|
@ -9,12 +9,18 @@ import type {
|
|||
CreateExceptionListItemSchema,
|
||||
CreateExceptionListSchema,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
ExceptionListSummarySchema,
|
||||
FoundExceptionListItemSchema,
|
||||
ListId,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
|
||||
import {
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
EXCEPTION_LIST_URL,
|
||||
INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL,
|
||||
} from '@kbn/securitysolution-list-constants';
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants';
|
||||
|
||||
|
@ -43,7 +49,7 @@ export class ExceptionsListApiClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* PrivateStatic method that creates the list and don't throw if list already exists.
|
||||
* PrivateStatic method that creates the list.
|
||||
* This method is being used when initializing an instance only once.
|
||||
*/
|
||||
private async createExceptionList(): Promise<void> {
|
||||
|
@ -55,19 +61,14 @@ export class ExceptionsListApiClient {
|
|||
new Promise<void>((resolve, reject) => {
|
||||
const asyncFunction = async () => {
|
||||
try {
|
||||
await this.http.post<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
|
||||
await this.http.post<ExceptionListSchema>(INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, {
|
||||
body: JSON.stringify({ ...this.listDefinition, list_id: this.listId }),
|
||||
});
|
||||
|
||||
resolve();
|
||||
} catch (err) {
|
||||
// Ignore 409 errors. List already created
|
||||
if (err.response?.status !== 409) {
|
||||
ExceptionsListApiClient.wasListCreated.delete(this.listId);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
ExceptionsListApiClient.wasListCreated.delete(this.listId);
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
asyncFunction();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue