[SIEM][Detection Engine][Lists] Adds specific endpoint_list REST API and API for abilities to auto-create the endpoint_list if it gets deleted (#71792)

* Adds specific endpoint_list REST API and API for abilities to autocreate the endpoint_list if it gets deleted

* Added the check against prepackaged list

* Updated to use LIST names

* Removed the namespace where it does not belong

* Updates per code review an extra space that was added

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Frank Hassanabad 2020-07-15 00:55:48 -06:00 committed by GitHub
parent ced455e077
commit 21156d6f18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1204 additions and 28 deletions

View file

@ -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';

View file

@ -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<typeof createEndpointListItemSchema>
>;
export type CreateEndpointListItemSchema = RequiredKeepUndefined<
t.TypeOf<typeof createEndpointListItemSchema>
>;
// This type is used after a decode since some things are defaults after a decode.
export type CreateEndpointListItemSchemaDecoded = Identity<
Omit<CreateEndpointListItemSchema, '_tags' | 'tags' | 'item_id' | 'entries' | 'comments'> & {
_tags: _Tags;
comments: CreateCommentsArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
}
>;

View file

@ -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<typeof deleteEndpointListItemSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type DeleteEndpointListItemSchemaDecoded = DeleteEndpointListItemSchema;

View file

@ -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<typeof findEndpointListItemSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type FindEndpointListItemSchemaPartialDecoded = t.TypeOf<typeof findEndpointListItemSchema>;
// 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<typeof findEndpointListItemSchema>
>;

View file

@ -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

View file

@ -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';

View file

@ -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<typeof readEndpointListItemSchema>;
// 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<ReadEndpointListItemSchemaPartial>;

View file

@ -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<typeof updateEndpointListItemSchema>
>;
export type UpdateEndpointListItemSchema = RequiredKeepUndefined<
t.TypeOf<typeof updateEndpointListItemSchema>
>;
// This type is used after a decode since some things are defaults after a decode.
export type UpdateEndpointListItemSchemaDecoded = Identity<
Omit<UpdateEndpointListItemSchema, '_tags' | 'tags' | 'entries' | 'comments'> & {
_tags: _Tags;
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
}
>;

View file

@ -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,
});
}
}
);
};

View file

@ -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,
});
}
}
);
};

View file

@ -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,
});
}
}
);
};

View file

@ -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,
});
}
}
);
};

View file

@ -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';

View file

@ -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);
};

View file

@ -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,
});
}
}
);
};

View file

@ -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,
});
}
}
);
};

View file

@ -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) {

View file

@ -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 .

View file

@ -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 .

View file

@ -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"]
}
]
}

View file

@ -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",

View file

@ -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 .

View file

@ -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 .

View file

@ -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 .

View file

@ -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 .;

View file

@ -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

View file

@ -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

View file

@ -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<ExceptionListSchema | null> => {
const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' });
const dateNow = new Date().toISOString();
try {
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(
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;
}
}
};

View file

@ -68,5 +68,5 @@ export const createExceptionList = async ({
type,
updated_by: user,
});
return transformSavedObjectToExceptionList({ namespaceType, savedObject });
return transformSavedObjectToExceptionList({ savedObject });
};

View file

@ -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<ExceptionListSchema | null> => {
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<ExceptionListItemSchema> => {
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<ExceptionListItemSchema | null> => {
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<ExceptionListItemSchema | null> => {
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<ExceptionListItemSchema | null> => {
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<FoundExceptionListItemSchema | null> => {
const { savedObjectsClient } = this;
await this.createEndpointList();
return findExceptionListItem({
filter,
listId: ENDPOINT_LIST_ID,
namespaceType: 'agnostic',
page,
perPage,
savedObjectsClient,
sortField,
sortOrder,
});
};
}

View file

@ -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;

View file

@ -48,7 +48,7 @@ export const findExceptionList = async ({
sortOrder,
type: savedObjectType,
});
return transformSavedObjectsToFoundExceptionList({ namespaceType, savedObjectsFindResponse });
return transformSavedObjectsToFoundExceptionList({ savedObjectsFindResponse });
};
export const getExceptionListFilter = ({

View file

@ -35,7 +35,7 @@ export const getExceptionList = async ({
if (id != null) {
try {
const savedObject = await savedObjectsClient.get<ExceptionListSoSchema>(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 {

View file

@ -69,6 +69,6 @@ export const updateExceptionList = async ({
updated_by: user,
}
);
return transformSavedObjectUpdateToExceptionList({ exceptionList, namespaceType, savedObject });
return transformSavedObjectUpdateToExceptionList({ exceptionList, savedObject });
}
};

View file

@ -93,7 +93,6 @@ export const updateExceptionListItem = async ({
);
return transformSavedObjectUpdateToExceptionListItem({
exceptionListItem,
namespaceType,
savedObject,
});
}

View file

@ -67,10 +67,8 @@ export const getSavedObjectTypes = ({
export const transformSavedObjectToExceptionList = ({
savedObject,
namespaceType,
}: {
savedObject: SavedObject<ExceptionListSoSchema>;
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<ExceptionListSoSchema>;
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<ExceptionListSoSchema>;
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<ExceptionListSoSchema>;
namespaceType: NamespaceType;
}): FoundExceptionListSchema => {
return {
data: savedObjectsFindResponse.saved_objects.map((savedObject) =>
transformSavedObjectToExceptionList({ namespaceType, savedObject })
transformSavedObjectToExceptionList({ savedObject })
),
page: savedObjectsFindResponse.page,
per_page: savedObjectsFindResponse.per_page,

View file

@ -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);

View file

@ -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,