diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index af29b3aa53ded..7bb83cddd4331 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -23,3 +23,28 @@ export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; */ export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; + +/** + * Specific routes for the single global space agnostic endpoint list + */ +export const ENDPOINT_LIST_URL = '/api/endpoint_list'; + +/** + * Specific routes for the single global space agnostic endpoint list. These are convenience + * routes where they are going to try and create the global space agnostic endpoint list if it + * does not exist yet or if it was deleted at some point and re-create it before adding items to + * the list + */ +export const ENDPOINT_LIST_ITEM_URL = '/api/endpoint_list/items'; + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; + +/** The name of the single global space agnostic endpoint list */ +export const ENDPOINT_LIST_NAME = 'Elastic Endpoint Exception List'; + +/** The description of the single global space agnostic endpoint list */ +export const ENDPOINT_LIST_DESCRIPTION = 'Elastic Endpoint Exception List'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..5311c7a43cdb5 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -0,0 +1,63 @@ +/* + * 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 { + ItemId, + Tags, + _Tags, + _tags, + description, + exceptionListItemType, + meta, + name, + tags, +} from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; +import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types'; +import { EntriesArray } from '../types/entries'; +import { DefaultUuid } from '../../siem_common_deps'; + +export const createEndpointListItemSchema = t.intersection([ + t.exact( + t.type({ + description, + name, + type: exceptionListItemType, + }) + ), + t.exact( + t.partial({ + _tags, // defaults to empty array if not set during decode + comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode + entries: DefaultEntryArray, // defaults to empty array if not set during decode + item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode + meta, // defaults to undefined if not set during decode + tags, // defaults to empty array if not set during decode + }) + ), +]); + +export type CreateEndpointListItemSchemaPartial = Identity< + t.TypeOf +>; +export type CreateEndpointListItemSchema = RequiredKeepUndefined< + t.TypeOf +>; + +// This type is used after a decode since some things are defaults after a decode. +export type CreateEndpointListItemSchemaDecoded = Identity< + Omit & { + _tags: _Tags; + comments: CreateCommentsArray; + tags: Tags; + item_id: ItemId; + entries: EntriesArray; + } +>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..311af3a4c0437 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -0,0 +1,23 @@ +/* + * 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 { id, item_id } from '../common/schemas'; + +export const deleteEndpointListItemSchema = t.exact( + t.partial({ + id, + item_id, + }) +); + +export type DeleteEndpointListItemSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type DeleteEndpointListItemSchemaDecoded = DeleteEndpointListItemSchema; diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..c9ee46994d720 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * 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 { filter, sort_field, sort_order } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; +import { StringToPositiveNumber } from '../types/string_to_positive_number'; + +export const findEndpointListItemSchema = t.exact( + t.partial({ + filter, // defaults to undefined if not set during decode + page: StringToPositiveNumber, // defaults to undefined if not set during decode + per_page: StringToPositiveNumber, // defaults to undefined if not set during decode + sort_field, // defaults to undefined if not set during decode + sort_order, // defaults to undefined if not set during decode + }) +); + +export type FindEndpointListItemSchemaPartial = t.OutputOf; + +// This type is used after a decode since some things are defaults after a decode. +export type FindEndpointListItemSchemaPartialDecoded = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type FindEndpointListItemSchemaDecoded = RequiredKeepUndefined< + FindEndpointListItemSchemaPartialDecoded +>; + +export type FindEndpointListItemSchema = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 826da972fe7a3..aa53fa0fd912c 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -26,7 +26,7 @@ export const findExceptionListItemSchema = t.intersection([ ), t.exact( t.partial({ - filter: EmptyStringArray, // defaults to undefined if not set during decode + filter: EmptyStringArray, // defaults to an empty array [] if not set during decode namespace_type: DefaultNamespaceArray, // defaults to ['single'] if not set during decode page: StringToPositiveNumber, // defaults to undefined if not set during decode per_page: StringToPositiveNumber, // defaults to undefined if not set during decode diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts index 7ab3d943f14da..172d73a5c7377 100644 --- a/x-pack/plugins/lists/common/schemas/request/index.ts +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './create_endpoint_list_item_schema'; export * from './create_exception_list_item_schema'; export * from './create_exception_list_schema'; export * from './create_list_item_schema'; export * from './create_list_schema'; +export * from './delete_endpoint_list_item_schema'; export * from './delete_exception_list_item_schema'; export * from './delete_exception_list_schema'; export * from './delete_list_item_schema'; export * from './delete_list_schema'; export * from './export_list_item_query_schema'; +export * from './find_endpoint_list_item_schema'; export * from './find_exception_list_item_schema'; export * from './find_exception_list_schema'; export * from './find_list_item_schema'; @@ -20,10 +23,12 @@ export * from './find_list_schema'; export * from './import_list_item_schema'; export * from './patch_list_item_schema'; export * from './patch_list_schema'; -export * from './read_exception_list_item_schema'; +export * from './read_endpoint_list_item_schema'; export * from './read_exception_list_schema'; +export * from './read_exception_list_item_schema'; export * from './read_list_item_schema'; export * from './read_list_schema'; +export * from './update_endpoint_list_item_schema'; export * from './update_exception_list_item_schema'; export * from './update_exception_list_schema'; export * from './import_list_item_query_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..22750f5db6a1d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -0,0 +1,31 @@ +/* + * 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 { id, item_id } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; + +export const readEndpointListItemSchema = t.exact( + t.partial({ + id, + item_id, + }) +); + +export type ReadEndpointListItemSchemaPartial = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadEndpointListItemSchemaPartialDecoded = ReadEndpointListItemSchemaPartial; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadEndpointListItemSchemaDecoded = RequiredKeepUndefined< + ReadEndpointListItemSchemaPartialDecoded +>; + +export type ReadEndpointListItemSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..dbe38f6d468e2 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts @@ -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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + Tags, + _Tags, + _tags, + description, + exceptionListItemType, + id, + meta, + name, + tags, +} from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; +import { + DefaultEntryArray, + DefaultUpdateCommentsArray, + EntriesArray, + UpdateCommentsArray, +} from '../types'; + +export const updateEndpointListItemSchema = t.intersection([ + t.exact( + t.type({ + description, + name, + type: exceptionListItemType, + }) + ), + t.exact( + t.partial({ + _tags, // defaults to empty array if not set during decode + comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode + entries: DefaultEntryArray, // defaults to empty array if not set during decode + id, // defaults to undefined if not set during decode + item_id: t.union([t.string, t.undefined]), + meta, // defaults to undefined if not set during decode + tags, // defaults to empty array if not set during decode + }) + ), +]); + +export type UpdateEndpointListItemSchemaPartial = Identity< + t.TypeOf +>; +export type UpdateEndpointListItemSchema = RequiredKeepUndefined< + t.TypeOf +>; + +// This type is used after a decode since some things are defaults after a decode. +export type UpdateEndpointListItemSchemaDecoded = Identity< + Omit & { + _tags: _Tags; + comments: UpdateCommentsArray; + tags: Tags; + entries: EntriesArray; + } +>; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..b6eacc3b7dd04 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -0,0 +1,86 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + CreateEndpointListItemSchemaDecoded, + createEndpointListItemSchema, + exceptionListItemSchema, +} from '../../common/schemas'; + +import { getExceptionListClient } from './utils/get_exception_list_client'; + +export const createEndpointListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + body: buildRouteValidation< + typeof createEndpointListItemSchema, + CreateEndpointListItemSchemaDecoded + >(createEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + name, + _tags, + tags, + meta, + comments, + description, + entries, + item_id: itemId, + type, + } = request.body; + const exceptionLists = getExceptionListClient(context); + const exceptionListItem = await exceptionLists.getEndpointListItem({ + id: undefined, + itemId, + }); + if (exceptionListItem != null) { + return siemResponse.error({ + body: `exception list item id: "${itemId}" already exists`, + statusCode: 409, + }); + } else { + const createdList = await exceptionLists.createEndpointListItem({ + _tags, + comments, + description, + entries, + itemId, + meta, + name, + tags, + type, + }); + const [validated, errors] = validate(createdList, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts new file mode 100644 index 0000000000000..5d0f3599729b3 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -0,0 +1,63 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; + +import { ENDPOINT_LIST_URL } from '../../common/constants'; +import { buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { exceptionListSchema } from '../../common/schemas'; + +import { getExceptionListClient } from './utils/get_exception_list_client'; + +/** + * This creates the endpoint list if it does not exist. If it does exist, + * this will conflict but continue. This is intended to be as fast as possible so it tries + * each and every time it is called to create the endpoint_list and just ignores any + * conflict so at worse case only one round trip happens per API call. If any error other than conflict + * happens this will return that error. If the list already exists this will return an empty + * object. + * @param router The router to use. + */ +export const createEndpointListRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_URL, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + try { + // Our goal is be fast as possible and block the least amount of + const exceptionLists = getExceptionListClient(context); + const createdList = await exceptionLists.createEndpointList(); + if (createdList != null) { + const [validated, errors] = validate(createdList, t.union([exceptionListSchema, t.null])); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else { + // We always return ok on a create endpoint list route but with an empty body as + // an additional fetch of the full list would be slower and the UI has everything hard coded + // within it to get the list if it needs details about it. + return response.ok({ body: {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..b8946c542b27e --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -0,0 +1,72 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + DeleteEndpointListItemSchemaDecoded, + deleteEndpointListItemSchema, + exceptionListItemSchema, +} from '../../common/schemas'; + +import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; + +export const deleteEndpointListItemRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + query: buildRouteValidation< + typeof deleteEndpointListItemSchema, + DeleteEndpointListItemSchemaDecoded + >(deleteEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = getExceptionListClient(context); + const { item_id: itemId, id } = request.query; + if (itemId == null && id == null) { + return siemResponse.error({ + body: 'Either "item_id" or "id" needs to be defined in the request', + statusCode: 400, + }); + } else { + const deleted = await exceptionLists.deleteEndpointListItem({ + id, + itemId, + }); + if (deleted == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..7374ff7dc92ea --- /dev/null +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -0,0 +1,77 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + FindEndpointListItemSchemaDecoded, + findEndpointListItemSchema, + foundExceptionListItemSchema, +} from '../../common/schemas'; + +import { getExceptionListClient } from './utils'; + +export const findEndpointListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: `${ENDPOINT_LIST_ITEM_URL}/_find`, + validate: { + query: buildRouteValidation< + typeof findEndpointListItemSchema, + FindEndpointListItemSchemaDecoded + >(findEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = getExceptionListClient(context); + const { + filter, + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; + + const exceptionListItems = await exceptionLists.findEndpointListItem({ + filter, + page, + perPage, + sortField, + sortOrder, + }); + if (exceptionListItems == null) { + // Although I have this line of code here, this is an incredibly rare thing to have + // happen as the findEndpointListItem tries to auto-create the endpoint list if + // does not exist. + return siemResponse.error({ + body: `list id: "${ENDPOINT_LIST_ID}" does not exist`, + statusCode: 404, + }); + } + const [validated, errors] = validate(exceptionListItems, foundExceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index 72117c46213fe..0d99d726d232d 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -4,17 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './create_endpoint_list_item_route'; +export * from './create_endpoint_list_route'; export * from './create_exception_list_item_route'; export * from './create_exception_list_route'; export * from './create_list_index_route'; export * from './create_list_item_route'; export * from './create_list_route'; +export * from './delete_endpoint_list_item_route'; export * from './delete_exception_list_route'; export * from './delete_exception_list_item_route'; export * from './delete_list_index_route'; export * from './delete_list_item_route'; export * from './delete_list_route'; export * from './export_list_item_route'; +export * from './find_endpoint_list_item_route'; export * from './find_exception_list_item_route'; export * from './find_exception_list_route'; export * from './find_list_item_route'; @@ -23,11 +27,14 @@ export * from './import_list_item_route'; export * from './init_routes'; export * from './patch_list_item_route'; export * from './patch_list_route'; +export * from './read_endpoint_list_item_route'; export * from './read_exception_list_item_route'; export * from './read_exception_list_route'; export * from './read_list_index_route'; export * from './read_list_item_route'; export * from './read_list_route'; +export * from './read_privileges_route'; +export * from './update_endpoint_list_item_route'; export * from './update_exception_list_item_route'; export * from './update_exception_list_route'; export * from './update_list_item_route'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index fef7f19f02df2..7e9e956ebf094 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -9,20 +9,22 @@ import { IRouter } from 'kibana/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ConfigType } from '../config'; -import { readPrivilegesRoute } from './read_privileges_route'; - import { + createEndpointListItemRoute, + createEndpointListRoute, createExceptionListItemRoute, createExceptionListRoute, createListIndexRoute, createListItemRoute, createListRoute, + deleteEndpointListItemRoute, deleteExceptionListItemRoute, deleteExceptionListRoute, deleteListIndexRoute, deleteListItemRoute, deleteListRoute, exportListItemRoute, + findEndpointListItemRoute, findExceptionListItemRoute, findExceptionListRoute, findListItemRoute, @@ -30,11 +32,14 @@ import { importListItemRoute, patchListItemRoute, patchListRoute, + readEndpointListItemRoute, readExceptionListItemRoute, readExceptionListRoute, readListIndexRoute, readListItemRoute, readListRoute, + readPrivilegesRoute, + updateEndpointListItemRoute, updateExceptionListItemRoute, updateExceptionListRoute, updateListItemRoute, @@ -83,4 +88,14 @@ export const initRoutes = ( updateExceptionListItemRoute(router); deleteExceptionListItemRoute(router); findExceptionListItemRoute(router); + + // endpoint list + createEndpointListRoute(router); + + // endpoint list items + createEndpointListItemRoute(router); + readEndpointListItemRoute(router); + updateEndpointListItemRoute(router); + deleteEndpointListItemRoute(router); + findEndpointListItemRoute(router); }; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..5e7ed901bf0cb --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -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 { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + ReadEndpointListItemSchemaDecoded, + exceptionListItemSchema, + readEndpointListItemSchema, +} from '../../common/schemas'; + +import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; + +export const readEndpointListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + query: buildRouteValidation< + typeof readEndpointListItemSchema, + ReadEndpointListItemSchemaDecoded + >(readEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, item_id: itemId } = request.query; + const exceptionLists = getExceptionListClient(context); + if (id != null || itemId != null) { + const exceptionListItem = await exceptionLists.getEndpointListItem({ + id, + itemId, + }); + if (exceptionListItem == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); + } else { + const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else { + return siemResponse.error({ body: 'id or item_id required', statusCode: 400 }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..1ecf4e8a9765d --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -0,0 +1,91 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + UpdateEndpointListItemSchemaDecoded, + exceptionListItemSchema, + updateEndpointListItemSchema, +} from '../../common/schemas'; + +import { getExceptionListClient } from '.'; + +export const updateEndpointListItemRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + body: buildRouteValidation< + typeof updateEndpointListItemSchema, + UpdateEndpointListItemSchemaDecoded + >(updateEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + description, + id, + name, + meta, + type, + _tags, + comments, + entries, + item_id: itemId, + tags, + } = request.body; + const exceptionLists = getExceptionListClient(context); + const exceptionListItem = await exceptionLists.updateEndpointListItem({ + _tags, + comments, + description, + entries, + id, + itemId, + meta, + name, + tags, + type, + }); + if (exceptionListItem == null) { + if (id != null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `list item item_id: "${itemId}" not found`, + statusCode: 404, + }); + } + } else { + const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 0ec33b7651982..f6c7bcebedc13 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -62,10 +62,17 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { type, }); if (exceptionListItem == null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, - }); + if (id != null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `list item item_id: "${itemId}" not found`, + statusCode: 404, + }); + } } else { const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); if (errors != null) { diff --git a/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh new file mode 100755 index 0000000000000..b668869bbd82f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_endpoint_list_item.sh ${item_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?item_id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh new file mode 100755 index 0000000000000..86dcd0ff1debc --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_endpoint_list_item_by_id.sh ${list_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json new file mode 100644 index 0000000000000..8ccbe707f204c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json @@ -0,0 +1,21 @@ +{ + "item_id": "simple_list_item", + "_tags": ["endpoint", "process", "malware", "os:linux"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample endpoint type exception", + "name": "Sample Endpoint Exception List", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "excluded", + "type": "exists" + }, + { + "field": "host.name", + "operator": "included", + "type": "match_any", + "value": ["some host", "another host"] + } + ] +} diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json index 08bd95b7d124c..da345fb930c04 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json @@ -1,5 +1,5 @@ { - "item_id": "endpoint_list_item", + "item_id": "simple_list_item", "_tags": ["endpoint", "process", "malware", "os:windows"], "tags": ["user added string for a tag", "malware"], "type": "simple", diff --git a/x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh b/x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh new file mode 100755 index 0000000000000..9372389a70b01 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Optionally, post at least one list item +# ./post_endpoint_list_item.sh ./exception_lists/new/endpoint_list_item.json +# +# Then you can query it as in: +# Example: ./find_endpoint_list_item.sh +# +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items/_find" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh new file mode 100755 index 0000000000000..4f5842048293a --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_endpoint_list_item.sh ${item_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?item_id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh new file mode 100755 index 0000000000000..6e035010014a1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +set -e +./check_env_variables.sh + +# Example: ./get_endpoint_list_item.sh ${id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/post_endpoint_list.sh b/x-pack/plugins/lists/server/scripts/post_endpoint_list.sh new file mode 100755 index 0000000000000..e0b179f443547 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_endpoint_list.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./exception_lists/new/exception_list.json}) + +# Example: ./post_endpoint_list.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/endpoint_list \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh b/x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh new file mode 100755 index 0000000000000..8235a2ec06eb7 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./exception_lists/new/endpoint_list_item.json}) + +# Example: ./post_endpoint_list_item.sh +# Example: ./post_endpoint_list_item.sh ./exception_lists/new/endpoint_list_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_endpoint_item.sh b/x-pack/plugins/lists/server/scripts/update_endpoint_item.sh new file mode 100755 index 0000000000000..4a6ca3881a323 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_endpoint_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./exception_lists/updates/simple_update_item.json}) + +# Example: ./update_endpoint_list_item.sh +# Example: ./update_endpoint_list_item.sh ./exception_lists/updates/simple_update_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts new file mode 100644 index 0000000000000..b9a0194e20074 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts @@ -0,0 +1,65 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import uuid from 'uuid'; + +import { + ENDPOINT_LIST_DESCRIPTION, + ENDPOINT_LIST_ID, + ENDPOINT_LIST_NAME, +} from '../../../common/constants'; +import { ExceptionListSchema, ExceptionListSoSchema } from '../../../common/schemas'; + +import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; + +interface CreateEndpointListOptions { + savedObjectsClient: SavedObjectsClientContract; + user: string; + tieBreaker?: string; +} + +export const createEndpointList = async ({ + savedObjectsClient, + user, + tieBreaker, +}: CreateEndpointListOptions): Promise => { + const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); + const dateNow = new Date().toISOString(); + try { + const savedObject = await savedObjectsClient.create( + savedObjectType, + { + _tags: [], + comments: undefined, + created_at: dateNow, + created_by: user, + description: ENDPOINT_LIST_DESCRIPTION, + entries: undefined, + item_id: undefined, + list_id: ENDPOINT_LIST_ID, + list_type: 'list', + meta: undefined, + name: ENDPOINT_LIST_NAME, + tags: [], + tie_breaker_id: tieBreaker ?? uuid.v4(), + type: 'endpoint', + updated_by: user, + }, + { + // We intentionally hard coding the id so that there can only be one exception list within the space + id: ENDPOINT_LIST_ID, + } + ); + return transformSavedObjectToExceptionList({ savedObject }); + } catch (err) { + if (err.status === 409) { + return null; + } else { + throw err; + } + } +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index f6a3bca10028d..4da74c7df48bf 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -68,5 +68,5 @@ export const createExceptionList = async ({ type, updated_by: user, }); - return transformSavedObjectToExceptionList({ namespaceType, savedObject }); + return transformSavedObjectToExceptionList({ savedObject }); }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 62afda52bd79d..5c9607e2d956d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -6,6 +6,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; +import { ENDPOINT_LIST_ID } from '../../../common/constants'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -15,15 +16,20 @@ import { import { ConstructorOptions, + CreateEndpointListItemOptions, CreateExceptionListItemOptions, CreateExceptionListOptions, + DeleteEndpointListItemOptions, DeleteExceptionListItemOptions, DeleteExceptionListOptions, + FindEndpointListItemOptions, FindExceptionListItemOptions, FindExceptionListOptions, FindExceptionListsItemOptions, + GetEndpointListItemOptions, GetExceptionListItemOptions, GetExceptionListOptions, + UpdateEndpointListItemOptions, UpdateExceptionListItemOptions, UpdateExceptionListOptions, } from './exception_list_client_types'; @@ -38,6 +44,7 @@ import { deleteExceptionListItem } from './delete_exception_list_item'; import { findExceptionListItem } from './find_exception_list_item'; import { findExceptionList } from './find_exception_list'; import { findExceptionListsItem } from './find_exception_list_items'; +import { createEndpointList } from './create_endpoint_list'; export class ExceptionListClient { private readonly user: string; @@ -67,6 +74,103 @@ export class ExceptionListClient { return getExceptionListItem({ id, itemId, namespaceType, savedObjectsClient }); }; + /** + * This creates an agnostic space endpoint list if it does not exist. This tries to be + * as fast as possible by ignoring conflict errors and not returning the contents of the + * list if it already exists. + * @returns ExceptionListSchema if it created the endpoint list, otherwise null if it already exists + */ + public createEndpointList = async (): Promise => { + const { savedObjectsClient, user } = this; + return createEndpointList({ + savedObjectsClient, + user, + }); + }; + + /** + * This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will + * auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint + * being there and existing before the item is inserted into the agnostic endpoint list. + */ + public createEndpointListItem = async ({ + _tags, + comments, + description, + entries, + itemId, + meta, + name, + tags, + type, + }: CreateEndpointListItemOptions): Promise => { + const { savedObjectsClient, user } = this; + await this.createEndpointList(); + return createExceptionListItem({ + _tags, + comments, + description, + entries, + itemId, + listId: ENDPOINT_LIST_ID, + meta, + name, + namespaceType: 'agnostic', + savedObjectsClient, + tags, + type, + user, + }); + }; + + /** + * This is the same as "updateListItem" except it applies specifically to the endpoint list and will + * auto-call the "createEndpointList" for you so that you have the best chance of the endpoint + * being there if it did not exist before. If the list did not exist before, then creating it here will still cause a + * return of null but at least the list exists again. + */ + public updateEndpointListItem = async ({ + _tags, + comments, + description, + entries, + id, + itemId, + meta, + name, + tags, + type, + }: UpdateEndpointListItemOptions): Promise => { + const { savedObjectsClient, user } = this; + await this.createEndpointList(); + return updateExceptionListItem({ + _tags, + comments, + description, + entries, + id, + itemId, + meta, + name, + namespaceType: 'agnostic', + savedObjectsClient, + tags, + type, + user, + }); + }; + + /** + * This is the same as "getExceptionListItem" except it applies specifically to the endpoint list. + */ + public getEndpointListItem = async ({ + itemId, + id, + }: GetEndpointListItemOptions): Promise => { + const { savedObjectsClient } = this; + return getExceptionListItem({ id, itemId, namespaceType: 'agnostic', savedObjectsClient }); + }; + public createExceptionList = async ({ _tags, description, @@ -209,6 +313,22 @@ export class ExceptionListClient { }); }; + /** + * This is the same as "deleteExceptionListItem" except it applies specifically to the endpoint list. + */ + public deleteEndpointListItem = async ({ + id, + itemId, + }: DeleteEndpointListItemOptions): Promise => { + const { savedObjectsClient } = this; + return deleteExceptionListItem({ + id, + itemId, + namespaceType: 'agnostic', + savedObjectsClient, + }); + }; + public findExceptionListItem = async ({ listId, filter, @@ -272,4 +392,33 @@ export class ExceptionListClient { sortOrder, }); }; + + /** + * This is the same as "findExceptionList" except it applies specifically to the endpoint list and will + * auto-call the "createEndpointList" for you so that you have the best chance of the endpoint + * being there if it did not exist before. If the list did not exist before, then creating it here should give you + * a good guarantee that you will get an empty record set rather than null. I keep the null as the return value in + * the off chance that you still might somehow not get into a race condition where the endpoint list does + * not exist because someone deleted it in-between the initial create and then the find. + */ + public findEndpointListItem = async ({ + filter, + perPage, + page, + sortField, + sortOrder, + }: FindEndpointListItemOptions): Promise => { + const { savedObjectsClient } = this; + await this.createEndpointList(); + return findExceptionListItem({ + filter, + listId: ENDPOINT_LIST_ID, + namespaceType: 'agnostic', + page, + perPage, + savedObjectsClient, + sortField, + sortOrder, + }); + }; } diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index b3070f2d4a70d..89f8310281648 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -86,12 +86,22 @@ export interface DeleteExceptionListItemOptions { namespaceType: NamespaceType; } +export interface DeleteEndpointListItemOptions { + id: IdOrUndefined; + itemId: ItemIdOrUndefined; +} + export interface GetExceptionListItemOptions { itemId: ItemIdOrUndefined; id: IdOrUndefined; namespaceType: NamespaceType; } +export interface GetEndpointListItemOptions { + itemId: ItemIdOrUndefined; + id: IdOrUndefined; +} + export interface CreateExceptionListItemOptions { _tags: _Tags; comments: CreateCommentsArray; @@ -106,6 +116,18 @@ export interface CreateExceptionListItemOptions { type: ExceptionListItemType; } +export interface CreateEndpointListItemOptions { + _tags: _Tags; + comments: CreateCommentsArray; + entries: EntriesArray; + itemId: ItemId; + name: Name; + description: Description; + meta: MetaOrUndefined; + tags: Tags; + type: ExceptionListItemType; +} + export interface UpdateExceptionListItemOptions { _tags: _TagsOrUndefined; comments: UpdateCommentsArray; @@ -120,6 +142,19 @@ export interface UpdateExceptionListItemOptions { type: ExceptionListItemTypeOrUndefined; } +export interface UpdateEndpointListItemOptions { + _tags: _TagsOrUndefined; + comments: UpdateCommentsArray; + entries: EntriesArrayOrUndefined; + id: IdOrUndefined; + itemId: ItemIdOrUndefined; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; + tags: TagsOrUndefined; + type: ExceptionListItemTypeOrUndefined; +} + export interface FindExceptionListItemOptions { listId: ListId; namespaceType: NamespaceType; @@ -130,6 +165,14 @@ export interface FindExceptionListItemOptions { sortOrder: SortOrderOrUndefined; } +export interface FindEndpointListItemOptions { + filter: FilterOrUndefined; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} + export interface FindExceptionListsItemOptions { listId: NonEmptyStringArrayDecoded; namespaceType: NamespaceTypeArray; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 899ed30863770..84cc7ba2f1021 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -48,7 +48,7 @@ export const findExceptionList = async ({ sortOrder, type: savedObjectType, }); - return transformSavedObjectsToFoundExceptionList({ namespaceType, savedObjectsFindResponse }); + return transformSavedObjectsToFoundExceptionList({ savedObjectsFindResponse }); }; export const getExceptionListFilter = ({ diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 8f511d140b0ff..a5c1e2e5c6bc9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -35,7 +35,7 @@ export const getExceptionList = async ({ if (id != null) { try { const savedObject = await savedObjectsClient.get(savedObjectType, id); - return transformSavedObjectToExceptionList({ namespaceType, savedObject }); + return transformSavedObjectToExceptionList({ savedObject }); } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return null; @@ -55,7 +55,6 @@ export const getExceptionList = async ({ }); if (savedObject.saved_objects[0] != null) { return transformSavedObjectToExceptionList({ - namespaceType, savedObject: savedObject.saved_objects[0], }); } else { diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index e4d6718ddc29f..a739366c67331 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -69,6 +69,6 @@ export const updateExceptionList = async ({ updated_by: user, } ); - return transformSavedObjectUpdateToExceptionList({ exceptionList, namespaceType, savedObject }); + return transformSavedObjectUpdateToExceptionList({ exceptionList, savedObject }); } }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 2059c730d809f..a5ed1e38df374 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -93,7 +93,6 @@ export const updateExceptionListItem = async ({ ); return transformSavedObjectUpdateToExceptionListItem({ exceptionListItem, - namespaceType, savedObject, }); } diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 3ef2c337e80b6..ded39933fe9d8 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -67,10 +67,8 @@ export const getSavedObjectTypes = ({ export const transformSavedObjectToExceptionList = ({ savedObject, - namespaceType, }: { savedObject: SavedObject; - namespaceType: NamespaceType; }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { @@ -102,7 +100,7 @@ export const transformSavedObjectToExceptionList = ({ list_id, meta, name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags, tie_breaker_id, type: exceptionListType.is(type) ? type : 'detection', @@ -114,11 +112,9 @@ export const transformSavedObjectToExceptionList = ({ export const transformSavedObjectUpdateToExceptionList = ({ exceptionList, savedObject, - namespaceType, }: { exceptionList: ExceptionListSchema; savedObject: SavedObjectsUpdateResponse; - namespaceType: NamespaceType; }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { @@ -138,7 +134,7 @@ export const transformSavedObjectUpdateToExceptionList = ({ list_id: exceptionList.list_id, meta: meta ?? exceptionList.meta, name: name ?? exceptionList.name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags: tags ?? exceptionList.tags, tie_breaker_id: exceptionList.tie_breaker_id, type: exceptionListType.is(type) ? type : exceptionList.type, @@ -200,11 +196,9 @@ export const transformSavedObjectToExceptionListItem = ({ export const transformSavedObjectUpdateToExceptionListItem = ({ exceptionListItem, savedObject, - namespaceType, }: { exceptionListItem: ExceptionListItemSchema; savedObject: SavedObjectsUpdateResponse; - namespaceType: NamespaceType; }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { @@ -239,7 +233,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ list_id: exceptionListItem.list_id, meta: meta ?? exceptionListItem.meta, name: name ?? exceptionListItem.name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags: tags ?? exceptionListItem.tags, tie_breaker_id: exceptionListItem.tie_breaker_id, type: exceptionListItemType.is(type) ? type : exceptionListItem.type, @@ -265,14 +259,12 @@ export const transformSavedObjectsToFoundExceptionListItem = ({ export const transformSavedObjectsToFoundExceptionList = ({ savedObjectsFindResponse, - namespaceType, }: { savedObjectsFindResponse: SavedObjectsFindResponse; - namespaceType: NamespaceType; }): FoundExceptionListSchema => { return { data: savedObjectsFindResponse.saved_objects.map((savedObject) => - transformSavedObjectToExceptionList({ namespaceType, savedObject }) + transformSavedObjectToExceptionList({ savedObject }) ), page: savedObjectsFindResponse.page, per_page: savedObjectsFindResponse.per_page, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 1226be71f63f5..b1f6f73b09627 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -55,6 +55,10 @@ export const addPrepackedRulesRoute = ( if (!siemClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } + + // This will create the endpoint list if it does not exist yet + await context.lists?.getExceptionListClient().createEndpointList(); + const rulesFromFileSystem = getPrepackagedRules(); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index edad3dd8a4f21..482edb9925557 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -97,7 +97,6 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void // TODO: Fix these either with an is conversion or by better typing them within io-ts const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - const alertsClient = context.alerting?.getAlertsClient(); const clusterClient = context.core.elasticsearch.legacy.client; const savedObjectsClient = context.core.savedObjects.client; @@ -127,6 +126,8 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }); } } + // This will create the endpoint list if it does not exist yet + await context.lists?.getExceptionListClient().createEndpointList(); const createdRule = await createRules({ alertsClient,