diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8f4abeb31c226..5c9f86f503c2e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -149,7 +149,7 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => { if (body != null && body.message) { if (body.statusCode != null) { throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]); + } else if (body.status_code != null) { + throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.status_code}`]); } else { throw new ToasterErrors([body.message]); } diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/legacy/plugins/siem/public/utils/api/index.ts index 1dc14413b04d2..3c70083136505 100644 --- a/x-pack/legacy/plugins/siem/public/utils/api/index.ts +++ b/x-pack/legacy/plugins/siem/public/utils/api/index.ts @@ -8,6 +8,7 @@ export interface MessageBody { error?: string; message?: string; statusCode?: number; + status_code?: number; } export const parseJsonFromBody = async (response: Response): Promise => { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index a488db3f0c3d7..bab7936005c04 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -13,7 +13,7 @@ import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_r import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route'; import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route'; import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; -import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; +import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route'; import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; import { ServerFacade } from './types'; @@ -23,12 +23,14 @@ import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_rout import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route'; import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; -import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route'; import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route'; import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route'; import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route'; import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; +import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; const APP_ID = 'siem'; @@ -50,12 +52,14 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy updateRulesRoute(__legacy); deleteRulesRoute(__legacy); findRulesRoute(__legacy); + patchRulesRoute(__legacy); addPrepackedRulesRoute(__legacy); getPrepackagedRulesStatusRoute(__legacy); createRulesBulkRoute(__legacy); updateRulesBulkRoute(__legacy); deleteRulesBulkRoute(__legacy); + patchRulesBulkRoute(__legacy); importRulesRoute(__legacy); exportRulesRoute(__legacy); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 65e1a79b67bf4..85570b1b59656 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -108,6 +108,14 @@ export const getUpdateRequest = (): ServerInjectOptions => ({ }, }); +export const getPatchRequest = (): ServerInjectOptions => ({ + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...typicalPayload(), + }, +}); + export const getReadRequest = (): ServerInjectOptions => ({ method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`, @@ -130,6 +138,12 @@ export const getUpdateBulkRequest = (): ServerInjectOptions => ({ payload: [typicalPayload()], }); +export const getPatchBulkRequest = (): ServerInjectOptions => ({ + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], +}); + export const getDeleteBulkRequest = (): ServerInjectOptions => ({ method: 'DELETE', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index 0eb090179b192..e0d48836013ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import Boom from 'boom'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import signalsPolicy from './signals_policy.json'; @@ -31,13 +30,18 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade) { + async handler(request: RequestFacade, headers) { try { const index = getIndex(request, server); const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, index); if (indexExists) { - return new Boom(`index: "${index}" already exists`, { statusCode: 409 }); + return headers + .response({ + message: `index: "${index}" already exists`, + status_code: 409, + }) + .code(409); } else { const policyExists = await getPolicyExists(callWithRequest, index); if (!policyExists) { @@ -52,7 +56,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = return { acknowledged: true }; } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index 82fe0f55215fb..c1edc824b81eb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import Boom from 'boom'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../../types'; @@ -39,13 +38,18 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade) { + async handler(request: RequestFacade, headers) { try { const index = getIndex(request, server); const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, index); if (!indexExists) { - return new Boom(`index: "${index}" does not exist`, { statusCode: 404 }); + return headers + .response({ + message: `index: "${index}" does not exist`, + status_code: 404, + }) + .code(404); } else { await deleteAllIndex(callWithRequest, `${index}-*`); const policyExists = await getPolicyExists(callWithRequest, index); @@ -59,7 +63,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = return { acknowledged: true }; } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index a8c4b7407c448..1a5018d446747 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import Boom from 'boom'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../../types'; @@ -42,11 +41,22 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => if (request.method.toLowerCase() === 'head') { return headers.response().code(404); } else { - return new Boom('index for this space does not exist', { statusCode: 404 }); + return headers + .response({ + message: 'index for this space does not exist', + status_code: 404, + }) + .code(404); } } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 5ea4dc7595b2b..45ecb7dc97288 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -24,7 +24,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }, }, }, - async handler(request: RulesRequest) { + async handler(request: RulesRequest, headers) { try { const callWithRequest = callWithRequestFactory(request, server); const index = getIndex(request, server); @@ -35,7 +35,13 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve has_encryption_key: !usingEphemeralEncryptionKey, }); } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 1014fbbfbcdaa..efc5d0ead6d90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -105,10 +105,9 @@ describe('add_prepackaged_rules_route', () => { alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ - error: 'Bad Request', message: 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', - statusCode: 400, + status_code: 400, }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 3e2337f2bb5a5..81f1d2a99d680 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import Boom from 'boom'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../../types'; @@ -55,9 +54,12 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex); if (!spaceIndexExists) { - return Boom.badRequest( - `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}` - ); + return headers + .response({ + message: `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`, + status_code: 400, + }) + .code(400); } } await Promise.all( @@ -75,7 +77,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR rules_updated: rulesToUpdate.length, }; } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 77c6f6f3b4840..98b1f9415ad4d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -91,9 +91,8 @@ describe('create_rules', () => { alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getCreateRequest()); expect(JSON.parse(payload)).toEqual({ - error: 'Bad Request', message: 'To create a rule, the index must exist first. Index .siem-signals does not exist', - statusCode: 400, + status_code: 400, }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 5953624925bbb..8f9ed88e62309 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import Boom from 'boom'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; @@ -15,7 +14,7 @@ import { createRulesSchema } from '../schemas/create_rules_schema'; import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { transformOrError } from './utils'; +import { transform } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; import { callWithRequestFactory, getIndex, transformError } from '../utils'; @@ -75,14 +74,22 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, finalIndex); if (!indexExists) { - return Boom.badRequest( - `To create a rule, the index must exist first. Index ${finalIndex} does not exist` - ); + return headers + .response({ + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + status_code: 400, + }) + .code(400); } if (ruleId != null) { const rule = await readRules({ alertsClient, ruleId }); if (rule != null) { - return Boom.conflict(`rule_id: "${ruleId}" already exists`); + return headers + .response({ + message: `rule_id: "${ruleId}" already exists`, + status_code: 409, + }) + .code(409); } } const createdRule = await createRules({ @@ -125,9 +132,25 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = search: `${createdRule.id}`, searchFields: ['alertId'], }); - return transformOrError(createdRule, ruleStatuses.saved_objects[0]); + const transformed = transform(createdRule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index b77ae56a486c8..bc62ac6d9df94 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -27,49 +27,51 @@ export const createDeleteRulesBulkRoute: Hapi.ServerRoute = { }, payload: queryRulesBulkSchema, }, - }, - async handler(request: QueryBulkRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !actionsClient || !savedObjectsClient) { - return headers.response().code(404); - } - const rules = Promise.all( - request.payload.map(async payloadRule => { - const { id, rule_id: ruleId } = payloadRule; - const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; - try { - const rule = await deleteRules({ - actionsClient, - alertsClient, - id, - ruleId, - }); - if (rule != null) { - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 6, - search: rule.id, - searchFields: ['alertId'], + async handler(request: QueryBulkRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + const rules = await Promise.all( + request.payload.map(async payloadRule => { + const { id, rule_id: ruleId } = payloadRule; + const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + try { + const rule = await deleteRules({ + actionsClient, + alertsClient, + id, + ruleId, }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); - return transformOrBulkError(idOrRuleIdOrUnknown, rule); - } else { - return getIdBulkError({ id, ruleId }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 6, + search: rule.id, + searchFields: ['alertId'], + }); + ruleStatuses.saved_objects.forEach(async obj => + savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) + ); + return transformOrBulkError(idOrRuleIdOrUnknown, rule); + } else { + return getIdBulkError({ id, ruleId }); + } + } catch (err) { + return transformBulkError(idOrRuleIdOrUnknown, err); } - } catch (err) { - return transformBulkError(idOrRuleIdOrUnknown, err); - } - }) - ); - return rules; + }) + ); + return rules; + }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index ea8b54a79a5e3..f1ae7fe80056e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -11,7 +11,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; import { ServerFacade } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -58,12 +58,34 @@ export const createDeleteRulesRoute: Hapi.ServerRoute = { ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - return transformOrError(rule, ruleStatuses.saved_objects[0]); + const transformed = transform(rule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } } else { - return getIdError({ id, ruleId }); + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index f9918301c6b3c..badcd25f15d33 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -14,6 +13,7 @@ import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_r import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; +import { transformError } from '../utils'; export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -42,11 +42,21 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = try { const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + return headers + .response({ + message: `Can't export more than ${exportSizeLimit} rules`, + status_code: 400, + }) + .code(400); } else { const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); if (nonPackagedRulesCount > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + return headers + .response({ + message: `Can't export more than ${exportSizeLimit} rules`, + status_code: 400, + }) + .code(400); } } @@ -62,8 +72,14 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = return response .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) .header('Content-Type', 'application/ndjson'); - } catch { - return Boom.badRequest(`Sorry, something went wrong to export rules`); + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 6cbc794751f9f..949453dedbe39 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -11,7 +11,7 @@ import { findRules } from '../../rules/find_rules'; import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; import { ServerFacade } from '../../../../types'; -import { transformFindAlertsOrError } from './utils'; +import { transformFindAlerts } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -60,9 +60,25 @@ export const createFindRulesRoute: Hapi.ServerRoute = { return results; }) ); - return transformFindAlertsOrError(rules, ruleStatuses); + const transformed = transformFindAlerts(rules, ruleStatuses); + if (transformed == null) { + return headers + .response({ + message: 'unknown data type, error transforming alert', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index faf0282296632..b3f13e48bf138 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -58,7 +58,13 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { rules_not_updated: rulesToUpdate.length, }; } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index c6096bea352a6..5843e290bb899 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import Hapi from 'hapi'; import { chunk, isEmpty, isFunction } from 'lodash/fp'; import { extname } from 'path'; @@ -24,17 +23,11 @@ import { } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; -import { updateRules } from '../../rules/update_rules'; +import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; type PromiseFromStreams = ImportRuleAlertRest | Error; -/* - * We were getting some error like that possible EventEmitter memory leak detected - * So we decide to batch the update by 10 to avoid any complication in the node side - * https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n - * - */ const CHUNK_PARSED_OBJECT_SIZE = 10; export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { @@ -70,13 +63,17 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const { filename } = request.payload.file.hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { - return Boom.badRequest(`Invalid file extension ${fileExtension}`); + return headers + .response({ + message: `Invalid file extension ${fileExtension}`, + status_code: 400, + }) + .code(400); } const objectLimit = server.config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); const parsedObjects = await createPromiseFromStreams([readStream]); - const uniqueParsedObjects = Array.from( parsedObjects .reduce( @@ -121,6 +118,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = } const { description, + enabled, false_positives: falsePositives, from, immutable, @@ -165,7 +163,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = alertsClient, actionsClient, description, - enabled: false, + enabled, falsePositives, from, immutable, @@ -193,12 +191,12 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { - await updateRules({ + await patchRules({ alertsClient, actionsClient, savedObjectsClient, description, - enabled: false, + enabled, falsePositives, from, immutable, @@ -231,7 +229,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = createBulkErrorObject({ ruleId, statusCode: 409, - message: `This Rule "${rule.name}" already exists`, + message: `rule_id: "${ruleId}" already exists`, }) ); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts new file mode 100644 index 0000000000000..aa0dd04786a2e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { + createMockServer, + createMockServerWithoutAlertClientDecoration, +} from '../__mocks__/_mock_server'; + +import { patchRulesRoute } from './patch_rules_route'; +import { ServerInjectOptions } from 'hapi'; + +import { + getFindResult, + getResult, + updateActionResult, + typicalPayload, + getFindResultWithSingleHit, + getPatchBulkRequest, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { patchRulesBulkRoute } from './patch_rules_bulk_route'; +import { BulkError } from '../utils'; + +describe('patch_rules_bulk', () => { + let { server, alertsClient, actionsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient } = createMockServer()); + patchRulesBulkRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getPatchBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 200 as a response when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getPatchBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 within the payload when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { payload } = await server.inject(getPatchBulkRequest()); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + patchRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if id is not given in either the body or the url', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + const { rule_id, ...noId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [noId], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns errors as 200 to just indicate ok something happened', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toEqual(200); + }); + + test('returns 404 in the payload if the record does not exist yet', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [ + { + ...noType, + type: 'something-made-up', + }, + ], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts new file mode 100644 index 0000000000000..846899877afb4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -0,0 +1,136 @@ +/* + * 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 Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { + BulkPatchRulesRequest, + IRuleSavedAttributesSavedObjectAttributes, +} from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { transformOrBulkError, getIdBulkError } from './utils'; +import { transformBulkError } from '../utils'; +import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; +import { patchRules } from '../../rules/patch_rules'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; + +export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PATCH', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: patchRulesBulkSchema, + }, + }, + async handler(request: BulkPatchRulesRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + + const rules = await Promise.all( + request.payload.map(async payloadRule => { + const { + description, + enabled, + false_positives: falsePositives, + from, + query, + language, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId, + timeline_title: timelineTitle, + meta, + filters, + rule_id: ruleId, + id, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + } = payloadRule; + const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + try { + const rule = await patchRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + query, + language, + outputIndex, + savedId, + savedObjectsClient, + timelineId, + timelineTitle, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); + } else { + return getIdBulkError({ id, ruleId }); + } + } catch (err) { + return transformBulkError(idOrRuleIdOrUnknown, err); + } + }) + ); + return rules; + }, + }; +}; + +export const patchRulesBulkRoute = (server: ServerFacade): void => { + server.route(createPatchRulesBulkRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts new file mode 100644 index 0000000000000..d315d45046e2d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -0,0 +1,129 @@ +/* + * 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 { + createMockServer, + createMockServerWithoutAlertClientDecoration, +} from '../__mocks__/_mock_server'; + +import { patchRulesRoute } from './patch_rules_route'; +import { ServerInjectOptions } from 'hapi'; + +import { + getFindResult, + getFindResultStatus, + getResult, + updateActionResult, + getPatchRequest, + typicalPayload, + getFindResultWithSingleHit, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; + +describe('patch_rules', () => { + let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); + patchRulesRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { statusCode } = await server.inject(getPatchRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { statusCode } = await server.inject(getPatchRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + patchRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if id is not given in either the body or the url', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { rule_id, ...noId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: { + payload: noId, + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 404 if the record does not exist yet', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...noType, + type: 'something-made-up', + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts new file mode 100644 index 0000000000000..054a54c6f33c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -0,0 +1,150 @@ +/* + * 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 Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { patchRules } from '../../rules/patch_rules'; +import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; +import { patchRulesSchema } from '../schemas/patch_rules_schema'; +import { ServerFacade } from '../../../../types'; +import { getIdError, transform } from './utils'; +import { transformError } from '../utils'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; + +export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PATCH', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: patchRulesSchema, + }, + }, + async handler(request: PatchRulesRequest, headers) { + const { + description, + enabled, + false_positives: falsePositives, + from, + query, + language, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId, + timeline_title: timelineTitle, + meta, + filters, + rule_id: ruleId, + id, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + } = request.payload; + + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + + try { + const rule = await patchRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + query, + language, + outputIndex, + savedId, + savedObjectsClient, + timelineId, + timelineTitle, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + const transformed = transform(rule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } + } else { + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } + }, + }; +}; + +export const patchRulesRoute = (server: ServerFacade) => { + server.route(createPatchRulesRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 25d8e6770f699..be32852b26ac1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -7,7 +7,7 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; @@ -55,12 +55,34 @@ export const createReadRulesRoute: Hapi.ServerRoute = { search: rule.id, searchFields: ['alertId'], }); - return transformOrError(rule, ruleStatuses.saved_objects[0]); + const transformedOrError = transform(rule, ruleStatuses.saved_objects[0]); + if (transformedOrError == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformedOrError; + } } else { - return getIdError({ id, ruleId }); + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 5f75696a368d3..2d86fede99609 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -13,10 +13,10 @@ import { } from '../../rules/types'; import { ServerFacade } from '../../../../types'; import { transformOrBulkError, getIdBulkError } from './utils'; -import { transformBulkError } from '../utils'; +import { transformBulkError, getIndex } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; -import { updateRules } from '../../rules/update_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { updateRules } from '../../rules/update_rules'; export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -43,7 +43,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou return headers.response().code(404); } - const rules = Promise.all( + const rules = await Promise.all( request.payload.map(async payloadRule => { const { description, @@ -73,6 +73,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou references, version, } = payloadRule; + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { const rule = await updateRules({ @@ -80,11 +81,12 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou actionsClient, description, enabled, + immutable: false, falsePositives, from, query, language, - outputIndex, + outputIndex: finalIndex, savedId, savedObjectsClient, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a36ae632de15b..fbb77e4d936e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -7,88 +7,47 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { updateRules } from '../../rules/update_rules'; import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; import { ServerFacade } from '../../../../types'; -import { getIdError, transformOrError } from './utils'; -import { transformError } from '../utils'; +import { getIdError, transform } from './utils'; +import { transformError, getIndex } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesRoute: Hapi.ServerRoute = { - method: 'PUT', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:siem'], - validate: { - options: { - abortEarly: false, +export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PUT', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: updateRulesSchema, }, - payload: updateRulesSchema, }, - }, - async handler(request: UpdateRulesRequest, headers) { - const { - description, - enabled, - false_positives: falsePositives, - from, - query, - language, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId = null, - timeline_title: timelineTitle = null, - meta, - filters, - rule_id: ruleId, - id, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - } = request.payload; - - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !actionsClient || !savedObjectsClient) { - return headers.response().code(404); - } - - try { - const rule = await updateRules({ - alertsClient, - actionsClient, + async handler(request: UpdateRulesRequest, headers) { + const { description, enabled, - falsePositives, + false_positives: falsePositives, from, query, language, - outputIndex, - savedId, - savedObjectsClient, - timelineId, - timelineTitle, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId = null, + timeline_title: timelineTitle = null, meta, filters, + rule_id: ruleId, id, - ruleId, index, interval, - maxSignals, - riskScore, + max_signals: maxSignals, + risk_score: riskScore, name, severity, tags, @@ -97,28 +56,97 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { threat, references, version, - }); - if (rule != null) { - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + } = request.payload; + + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const rule = await updateRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable: false, + query, + language, + outputIndex: finalIndex, + savedId, + savedObjectsClient, + timelineId, + timelineTitle, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, }); - return transformOrError(rule, ruleStatuses.saved_objects[0]); - } else { - return getIdError({ id, ruleId }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + const transformed = transform(rule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } + } else { + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } - } catch (err) { - return transformError(err); - } - }, + }, + }; }; export const updateRulesRoute = (server: ServerFacade) => { - server.route(createUpdateRulesRoute); + server.route(createUpdateRulesRoute(server)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index ec11a8fb2da39..7e7d67333e78d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - import { transformAlertToRule, getIdError, - transformFindAlertsOrError, - transformOrError, + transformFindAlerts, + transform, transformTags, getIdBulkError, transformOrBulkError, @@ -547,55 +545,87 @@ describe('utils', () => { }); describe('getIdError', () => { + test('it should have a status code', () => { + const error = getIdError({ id: '123', ruleId: undefined }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); + }); + test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { - const boom = getIdError({ id: '123', ruleId: undefined }); - expect(boom.message).toEqual('id: "123" not found'); + const error = getIdError({ id: '123', ruleId: undefined }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); }); test('outputs message about id not being found if only id is defined and ruleId is null', () => { - const boom = getIdError({ id: '123', ruleId: null }); - expect(boom.message).toEqual('id: "123" not found'); + const error = getIdError({ id: '123', ruleId: null }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); }); test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { - const boom = getIdError({ id: undefined, ruleId: 'rule-id-123' }); - expect(boom.message).toEqual('rule_id: "rule-id-123" not found'); + const error = getIdError({ id: undefined, ruleId: 'rule-id-123' }); + expect(error).toEqual({ + message: 'rule_id: "rule-id-123" not found', + statusCode: 404, + }); }); test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { - const boom = getIdError({ id: null, ruleId: 'rule-id-123' }); - expect(boom.message).toEqual('rule_id: "rule-id-123" not found'); + const error = getIdError({ id: null, ruleId: 'rule-id-123' }); + expect(error).toEqual({ + message: 'rule_id: "rule-id-123" not found', + statusCode: 404, + }); }); test('outputs message about both being not defined when both are undefined', () => { - const boom = getIdError({ id: undefined, ruleId: undefined }); - expect(boom.message).toEqual('id or rule_id should have been defined'); + const error = getIdError({ id: undefined, ruleId: undefined }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); test('outputs message about both being not defined when both are null', () => { - const boom = getIdError({ id: null, ruleId: null }); - expect(boom.message).toEqual('id or rule_id should have been defined'); + const error = getIdError({ id: null, ruleId: null }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); test('outputs message about both being not defined when id is null and ruleId is undefined', () => { - const boom = getIdError({ id: null, ruleId: undefined }); - expect(boom.message).toEqual('id or rule_id should have been defined'); + const error = getIdError({ id: null, ruleId: undefined }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); test('outputs message about both being not defined when id is undefined and ruleId is null', () => { - const boom = getIdError({ id: undefined, ruleId: null }); - expect(boom.message).toEqual('id or rule_id should have been defined'); + const error = getIdError({ id: undefined, ruleId: null }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); }); - describe('transformFindAlertsOrError', () => { + describe('transformFindAlerts', () => { test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlertsOrError({ data: [] }); + const output = transformFindAlerts({ data: [] }); expect(output).toEqual({ data: [] }); }); test('outputs 200 if the data is of type siem alert', () => { - const output = transformFindAlertsOrError({ + const output = transformFindAlerts({ data: [getResult()], }); const expected: OutputRuleAlertRest = { @@ -663,14 +693,14 @@ describe('utils', () => { }); test('returns 500 if the data is not of type siem alert', () => { - const output = transformFindAlertsOrError({ data: [{ random: 1 }] }); - expect((output as Boom).message).toEqual('Internal error transforming'); + const output = transformFindAlerts({ data: [{ random: 1 }] }); + expect(output).toBeNull(); }); }); describe('transformOrError', () => { test('outputs 200 if the data is of type siem alert', () => { - const output = transformOrError(getResult()); + const output = transform(getResult()); const expected: OutputRuleAlertRest = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', @@ -734,8 +764,8 @@ describe('utils', () => { }); test('returns 500 if the data is not of type siem alert', () => { - const output = transformOrError({ data: [{ random: 1 }] }); - expect((output as Boom).message).toEqual('Internal error transforming'); + const output = transform({ data: [{ random: 1 }] }); + expect(output).toBeNull(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index b45db53c13d88..abb94c10209dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { pickBy } from 'lodash/fp'; import { SavedObject } from 'kibana/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; @@ -24,6 +23,7 @@ import { createSuccessObject, ImportSuccessError, createImportErrorObject, + OutputError, } from '../utils'; export const getIdError = ({ @@ -32,13 +32,22 @@ export const getIdError = ({ }: { id: string | undefined | null; ruleId: string | undefined | null; -}) => { +}): OutputError => { if (id != null) { - return Boom.notFound(`id: "${id}" not found`); + return { + message: `id: "${id}" not found`, + statusCode: 404, + }; } else if (ruleId != null) { - return Boom.notFound(`rule_id: "${ruleId}" not found`); + return { + message: `rule_id: "${ruleId}" not found`, + statusCode: 404, + }; } else { - return Boom.notFound('id or rule_id should have been defined'); + return { + message: 'id or rule_id should have been defined', + statusCode: 404, + }; } }; @@ -136,10 +145,10 @@ export const transformAlertsToRules = ( return alerts.map(alert => transformAlertToRule(alert)); }; -export const transformFindAlertsOrError = ( +export const transformFindAlerts = ( findResults: { data: unknown[] }, ruleStatuses?: unknown[] -): unknown | Boom => { +): unknown | null => { if (!ruleStatuses && isAlertTypes(findResults.data)) { findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; @@ -150,14 +159,14 @@ export const transformFindAlertsOrError = ( ); return findResults; } else { - return new Boom('Internal error transforming', { statusCode: 500 }); + return null; } }; -export const transformOrError = ( +export const transform = ( alert: unknown, ruleStatus?: unknown -): Partial | Boom => { +): Partial | null => { if (!ruleStatus && isAlertType(alert)) { return transformAlertToRule(alert); } @@ -166,7 +175,7 @@ export const transformOrError = ( } else if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { return transformAlertToRule(alert, ruleStatus); } else { - return new Boom('Internal error transforming', { statusCode: 500 }); + return null; } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts index 1eab50848b822..2a64478962ced 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts @@ -5,7 +5,7 @@ */ import { createRulesBulkSchema } from './create_rules_bulk_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; // only the basics of testing are here. // see: create_rules_schema.test.ts for the bulk of the validation tests @@ -13,7 +13,7 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types'; describe('create_rules_bulk_schema', () => { test('can take an empty array and validate it', () => { expect( - createRulesBulkSchema.validate>>([]).error + createRulesBulkSchema.validate>>([]).error ).toBeFalsy(); }); @@ -29,7 +29,7 @@ describe('create_rules_bulk_schema', () => { test('single array of [id] does validate', () => { expect( - createRulesBulkSchema.validate>>([ + createRulesBulkSchema.validate>>([ { rule_id: 'rule-1', risk_score: 50, @@ -49,7 +49,7 @@ describe('create_rules_bulk_schema', () => { test('two values of [id] does validate', () => { expect( - createRulesBulkSchema.validate>>([ + createRulesBulkSchema.validate>>([ { rule_id: 'rule-1', risk_score: 50, @@ -82,7 +82,7 @@ describe('create_rules_bulk_schema', () => { test('The default for "from" will be "now-6m"', () => { expect( - createRulesBulkSchema.validate>([ + createRulesBulkSchema.validate>([ { rule_id: 'rule-1', risk_score: 50, @@ -102,7 +102,7 @@ describe('create_rules_bulk_schema', () => { test('The default for "to" will be "now"', () => { expect( - createRulesBulkSchema.validate>([ + createRulesBulkSchema.validate>([ { rule_id: 'rule-1', risk_score: 50, @@ -122,7 +122,7 @@ describe('create_rules_bulk_schema', () => { test('You cannot set the severity to a value other than low, medium, high, or critical', () => { expect( - createRulesBulkSchema.validate>([ + createRulesBulkSchema.validate>([ { rule_id: 'rule-1', risk_score: 50, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index d9605a265d28b..052a149f3d4dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -5,12 +5,12 @@ */ import { createRulesSchema } from './create_rules_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams, RuleAlertParamsRest } from '../../types'; describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createRulesSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts new file mode 100644 index 0000000000000..cbcb9eba75bc1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { patchRulesBulkSchema } from './patch_rules_bulk_schema'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; + +// only the basics of testing are here. +// see: patch_rules_schema.test.ts for the bulk of the validation tests +// this just wraps patchRulesSchema in an array +describe('patch_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + expect( + patchRulesBulkSchema.validate>>([]).error + ).toBeFalsy(); + }); + + test('made up values do not validate', () => { + expect( + patchRulesBulkSchema.validate<[{ madeUp: string }]>([ + { + madeUp: 'hi', + }, + ]).error + ).toBeTruthy(); + }); + + test('single array of [id] does validate', () => { + expect( + patchRulesBulkSchema.validate>>([ + { + id: 'rule-1', + }, + ]).error + ).toBeFalsy(); + }); + + test('two values of [id] does validate', () => { + expect( + patchRulesBulkSchema.validate>>([ + { + id: 'rule-1', + }, + { + id: 'rule-2', + }, + ]).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts new file mode 100644 index 0000000000000..ff813bce84add --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts @@ -0,0 +1,11 @@ +/* + * 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 Joi from 'joi'; + +import { patchRulesSchema } from './patch_rules_schema'; + +export const patchRulesBulkSchema = Joi.array().items(patchRulesSchema); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts new file mode 100644 index 0000000000000..11bed22e1c047 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -0,0 +1,1015 @@ +/* + * 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 { patchRulesSchema } from './patch_rules_schema'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { ThreatParams } from '../../types'; + +describe('patch rules schema', () => { + test('empty objects do not validate as they require at least id or rule_id', () => { + expect(patchRulesSchema.validate>({}).error).toBeTruthy(); + }); + + test('made up values do not validate', () => { + expect( + patchRulesSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[id] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[rule_id] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[id] and [rule_id] does not validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'id-1', + rule_id: 'rule-1', + }).error.message + ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); + }); + + test('[rule_id, description] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, description] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, risk_score] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + risk_score: 10, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('allows references to be sent as a valid value to patch with', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('does not default references to an array', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual(undefined); + }); + + test('does not default interval', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + type: 'query', + }).value.interval + ).toEqual(undefined); + }); + + test('does not default max signal', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(undefined); + }); + + test('references cannot be numbers', () => { + expect( + patchRulesSchema.validate< + Partial> & { references: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); + }); + + test('indexes cannot be numbers', () => { + expect( + patchRulesSchema.validate< + Partial> & { index: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); + }); + + test('saved_id is not required when type is saved_query and will validate without it', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + }).error + ).toBeFalsy(); + }); + + test('saved_id validates with saved_query', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type can have filters with it', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeFalsy(); + }); + + test('language validates with kuery', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error.message + ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); + }); + + test('max_signals cannot be negative', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); + }); + + test('max_signals cannot be zero', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); + }); + + test('max_signals can be 1', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('meta can be patched', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + meta: { whateverYouWant: 'anything_at_all' }, + }).error + ).toBeFalsy(); + }); + + test('You cannot patch meta as a string', () => { + expect( + patchRulesSchema.validate & { meta: string }>>( + { + id: 'rule-1', + meta: 'should not work', + } + ).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); + }); + + test('filters cannot be a string', () => { + expect( + patchRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + type: 'query', + filters: 'some string', + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); + }); + + test('threat is not defaulted to empty array on patch', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).value.threat + ).toBe(undefined); + }); + + test('threat is not defaulted to undefined on patch with empty array', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [], + }).value.threat + ).toMatchObject([]); + }); + + test('threat is valid when updated with all sub-objects', () => { + const expected: ThreatParams[] = [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ]; + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).value.threat + ).toMatchObject(expected); + }); + + test('threat is invalid when updated with missing property framework', () => { + expect( + patchRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); + }); + + test('threat is invalid when updated with missing tactic sub-object', () => { + expect( + patchRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + ); + }); + + test('threat is invalid when updated with missing technique', () => { + expect( + patchRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + framework: 'fake', + tactic: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' + ); + }); + + test('validates with timeline_id and timeline_title', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + timeline_title: 'some-title', + }).error + ).toBeFalsy(); + }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'timeline-id', + timeline_title: null, + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + timeline_title: '', + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: '', + timeline_title: 'some-title', + }).error.message + ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_title: 'some-title', + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); + }); + + test('You cannot set the severity to a value other than low, medium, high, or critical', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'junk', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).error.message + ).toEqual( + 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts new file mode 100644 index 0000000000000..d0ed1af01833b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -0,0 +1,67 @@ +/* + * 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 Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + enabled, + description, + false_positives, + filters, + from, + index, + rule_id, + interval, + query, + language, + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threat, + references, + id, + version, +} from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const patchRulesSchema = Joi.object({ + description, + enabled, + false_positives, + filters, + from, + rule_id, + id, + index, + interval, + query: query.allow(''), + language, + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threat, + references, + version, +}).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts index ab1ffaab49165..7ea7fcbd1d86b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts @@ -5,7 +5,7 @@ */ import { queryRulesBulkSchema } from './query_rules_bulk_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; // only the basics of testing are here. // see: query_rules_bulk_schema.test.ts for the bulk of the validation tests @@ -13,13 +13,13 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types'; describe('query_rules_bulk_schema', () => { test('can take an empty array and validate it', () => { expect( - queryRulesBulkSchema.validate>>([]).error + queryRulesBulkSchema.validate>>([]).error ).toBeFalsy(); }); test('both rule_id and id being supplied do not validate', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { rule_id: '1', id: '1', @@ -32,7 +32,7 @@ describe('query_rules_bulk_schema', () => { test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { id: '1', }, @@ -48,13 +48,13 @@ describe('query_rules_bulk_schema', () => { test('only id validates', () => { expect( - queryRulesBulkSchema.validate>>([{ id: '1' }]).error + queryRulesBulkSchema.validate>>([{ id: '1' }]).error ).toBeFalsy(); }); test('only id validates with two elements', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { id: '1' }, { id: '2' }, ]).error @@ -63,14 +63,14 @@ describe('query_rules_bulk_schema', () => { test('only rule_id validates', () => { expect( - queryRulesBulkSchema.validate>>([{ rule_id: '1' }]) + queryRulesBulkSchema.validate>>([{ rule_id: '1' }]) .error ).toBeFalsy(); }); test('only rule_id validates with two elements', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { rule_id: '1' }, { rule_id: '2' }, ]).error @@ -79,7 +79,7 @@ describe('query_rules_bulk_schema', () => { test('both id and rule_id validates with two separate elements', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { id: '1' }, { rule_id: '2' }, ]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts index c89d60e773a77..0f392e399f36c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts @@ -5,29 +5,29 @@ */ import { queryRulesSchema } from './query_rules_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; describe('queryRulesSchema', () => { test('empty objects do not validate', () => { - expect(queryRulesSchema.validate>({}).error).toBeTruthy(); + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); test('both rule_id and id being supplied do not validate', () => { expect( - queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error .message ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); }); test('only id validates', () => { expect( - queryRulesSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - queryRulesSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts index 2b1bad39eb686..e866260662ad7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts @@ -31,7 +31,17 @@ describe('update_rules_bulk_schema', () => { expect( updateRulesBulkSchema.validate>>([ { - id: 'rule-1', + id: 'id-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }, ]).error ).toBeFalsy(); @@ -41,10 +51,30 @@ describe('update_rules_bulk_schema', () => { expect( updateRulesBulkSchema.validate>>([ { - id: 'rule-1', + id: 'id-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }, { - id: 'rule-2', + id: 'id-2', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }, ]).error ).toBeFalsy(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index 0dc9f3df3da1c..c7899f3afa7b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -5,169 +5,107 @@ */ import { updateRulesSchema } from './update_rules_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams } from '../../types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { ThreatParams, RuleAlertParamsRest } from '../../types'; -describe('update rules schema', () => { +describe('create rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect(updateRulesSchema.validate>({}).error).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); }); - test('[id] does validate', () => { + test('[rule_id] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[rule_id] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[id and rule_id] does not validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'id-1', + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); }); - test('[rule_id, description] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, description] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, risk_score] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - risk_score: 10, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to] does validate', () => { + test('[id] and [rule_id] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ + id: 'id-1', rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', - }).error - ).toBeFalsy(); + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }).error.message + ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); }); - test('[id, description, from, to] does validate', () => { + test('[rule_id, description] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', - from: 'now-5m', - to: 'now', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name] does validate', () => { + test('[rule_id, description, from] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', - to: 'now', - name: 'some-name', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name] does validate', () => { + test('[rule_id, description, from, to] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', - name: 'some-name', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name, severity] does validate', () => { + test('[rule_id, description, from, to, name] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', - severity: 'low', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name, severity] does validate', () => { + test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name, severity, type] does validate', () => { + test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -176,56 +114,61 @@ describe('update rules schema', () => { severity: 'low', type: 'query', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type] does validate', () => { + test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', + interval: '5m', type: 'query', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', - interval: '5m', type: 'query', + interval: '5m', + index: ['index-1'], }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type, interval] does validate', () => { + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', - interval: '5m', type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -235,14 +178,17 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + query: 'some query', + language: 'kuery', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -251,14 +197,18 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + query: 'some query', + language: 'kuery', }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -268,14 +218,15 @@ describe('update rules schema', () => { interval: '5m', type: 'query', query: 'some query', + language: 'kuery', }).error ).toBeFalsy(); }); - test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -284,15 +235,17 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - query: 'some query', + risk_score: 50, }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -301,16 +254,16 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - query: 'some query', - language: 'kuery', }).error ).toBeFalsy(); }); - test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + test('You can send in an empty array to threat', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -319,16 +272,21 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + references: ['index-1'], query: 'some query', language: 'kuery', + max_signals: 1, + threat: [], }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -337,14 +295,33 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + threat: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], }).error ).toBeFalsy(); }); - test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { + test('allows references to be sent as valid', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -353,14 +330,19 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', }).error ).toBeFalsy(); }); - test('allows references to be sent as a valid value to update with', () => { + test('defaults references to an array', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -369,17 +351,20 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - references: ['index-1'], - query: 'some query', + query: 'some-query', language: 'kuery', - }).error - ).toBeFalsy(); + }).value.references + ).toEqual([]); }); - test('does not default references to an array', () => { + test('references cannot be numbers', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate< + Partial> & { references: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -390,47 +375,60 @@ describe('update rules schema', () => { type: 'query', query: 'some-query', language: 'kuery', - }).value.references - ).toEqual(undefined); + references: [5], + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); }); - test('does not default interval', () => { + test('indexes cannot be numbers', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - }).value.interval - ).toEqual(undefined); + updateRulesSchema.validate> & { index: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + } + ).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); }); - test('does not default max signal', () => { + test('defaults interval to 5 min', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', - interval: '5m', type: 'query', - }).value.max_signals - ).toEqual(undefined); + }).value.interval + ).toEqual('5m'); }); - test('references cannot be numbers', () => { + test('defaults max signals to 100', () => { expect( - updateRulesSchema.validate< - Partial> & { references: number[] } - >({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -439,41 +437,34 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - query: 'some-query', - language: 'kuery', - references: [5], - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); + }).value.max_signals + ).toEqual(100); }); - test('indexes cannot be numbers', () => { + test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - updateRulesSchema.validate< - Partial> & { index: number[] } - >({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', - index: [5], + index: ['index-1'], name: 'some-name', severity: 'low', interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', + type: 'saved_query', }).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); + ).toEqual('child "saved_id" fails because ["saved_id" is required]'); }); - test('saved_id is not required when type is saved_query and will validate without it', () => { + test('saved_id is required when type is saved_query and validates with it', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -482,14 +473,17 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'saved_query', + saved_id: 'some id', }).error ).toBeFalsy(); }); - test('saved_id validates with saved_query', () => { + test('saved_query type can have filters with it', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -499,14 +493,19 @@ describe('update rules schema', () => { interval: '5m', type: 'saved_query', saved_id: 'some id', + filters: [], }).error ).toBeFalsy(); }); - test('saved_query type can have filters with it', () => { + test('filters cannot be a string', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -516,15 +515,17 @@ describe('update rules schema', () => { interval: '5m', type: 'saved_query', saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); + filters: 'some string', + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); }); test('language validates with kuery', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -542,8 +543,10 @@ describe('update rules schema', () => { test('language validates with lucene', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -561,8 +564,10 @@ describe('update rules schema', () => { test('language does not validate with something made up', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -580,8 +585,10 @@ describe('update rules schema', () => { test('max_signals cannot be negative', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -600,8 +607,10 @@ describe('update rules schema', () => { test('max_signals cannot be zero', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -620,8 +629,10 @@ describe('update rules schema', () => { test('max_signals can be 1', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -638,42 +649,12 @@ describe('update rules schema', () => { ).toBeFalsy(); }); - test('meta can be updated', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - meta: { whateverYouWant: 'anything_at_all' }, - }).error - ).toBeFalsy(); - }); - - test('You cannot update meta as a string', () => { - expect( - updateRulesSchema.validate< - Partial & { meta: string }> - >({ - id: 'rule-1', - meta: 'should not work', - }).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('filters cannot be a string', () => { + test('You can optionally send in an array of tags', () => { expect( - updateRulesSchema.validate< - Partial & { filters: string }> - >({ + updateRulesSchema.validate>({ rule_id: 'rule-1', - type: 'query', - filters: 'some string', - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('threat is not defaulted to empty array on update', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -686,15 +667,18 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).value.threat - ).toBe(undefined); + tags: ['tag_1', 'tag_2'], + }).error + ).toBeFalsy(); }); - test('threat is not defaulted to undefined on update with empty array', () => { + test('You cannot send in an array of tags that are numbers', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', + updateRulesSchema.validate> & { tags: number[] }>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], @@ -706,32 +690,23 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - threat: [], - }).value.threat - ).toMatchObject([]); - }); - - test('threat is valid when updated with all sub-objects', () => { - const expected: ThreatParams[] = [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ]; + tags: [0, 1, 2], + }).error.message + ).toEqual( + 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]' + ); + }); + + test('You cannot send in an array of threat that are missing "framework"', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -746,7 +721,6 @@ describe('update rules schema', () => { max_signals: 1, threat: [ { - framework: 'fake', tactic: { id: 'fakeId', name: 'fakeName', @@ -761,18 +735,22 @@ describe('update rules schema', () => { ], }, ], - }).value.threat - ).toMatchObject(expected); + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); }); - test('threat is invalid when updated with missing property framework', () => { + test('You cannot send in an array of threat that are missing "tactic"', () => { expect( updateRulesSchema.validate< - Partial> & { - threat: Array>>; + Partial> & { + threat: Array>>; } >({ - id: 'rule-1', + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -787,11 +765,7 @@ describe('update rules schema', () => { max_signals: 1, threat: [ { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, + framework: 'fake', technique: [ { id: 'techniqueId', @@ -803,18 +777,20 @@ describe('update rules schema', () => { ], }).error.message ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' ); }); - test('threat is invalid when updated with missing tactic sub-object', () => { + test('You cannot send in an array of threat that are missing "technique"', () => { expect( updateRulesSchema.validate< - Partial> & { - threat: Array>>; + Partial> & { + threat: Array>>; } >({ - id: 'rule-1', + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -830,30 +806,52 @@ describe('update rules schema', () => { threat: [ { framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, }, ], }).error.message ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' ); }); - test('threat is invalid when updated with missing technique', () => { + test('You can optionally send in an array of false positives', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: ['false_1', 'false_2'], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of false positives that are numbers', () => { expect( updateRulesSchema.validate< - Partial> & { - threat: Array>>; - } + Partial> & { false_positives: number[] } >({ - id: 'rule-1', + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', + false_positives: [5, 4], from: 'now-5m', to: 'now', index: ['index-1'], @@ -865,26 +863,201 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - }, - ], }).error.message ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' + 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' ); }); + test('You cannot set the immutable when trying to create a rule', () => { + expect( + updateRulesSchema.validate< + Partial> & { immutable: number } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: 5, + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error.message + ).toEqual('"immutable" is not allowed'); + }); + + test('You cannot set the risk_score to 101', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 101, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); + }); + + test('You cannot set the risk_score to -1', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: -1, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); + }); + + test('You can set the risk_score to 0', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 0, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set the risk_score to 100', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 100, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set meta to any object you want', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }).error + ).toBeFalsy(); + }); + + test('You cannot create meta as a string', () => { + expect( + updateRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: 'should not work', + }).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); + }); + + test('You can omit the query string when filters are present', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + test('validates with timeline_id and timeline_title', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -892,18 +1065,22 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', - timeline_title: 'some-title', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }).error ).toBeFalsy(); }); test('You cannot omit timeline_title when timeline_id is present', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -911,17 +1088,21 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); }); test('You cannot have a null value for timeline_title when timeline_id is present', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -929,9 +1110,11 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'timeline-id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', timeline_title: null, }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); @@ -939,8 +1122,10 @@ describe('update rules schema', () => { test('You cannot have empty string for timeline_title when timeline_id is present', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -948,9 +1133,11 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', timeline_title: '', }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); @@ -958,8 +1145,10 @@ describe('update rules schema', () => { test('You cannot have timeline_title with an empty timeline_id', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -967,8 +1156,10 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', timeline_id: '', timeline_title: 'some-title', }).error.message @@ -977,8 +1168,10 @@ describe('update rules schema', () => { test('You cannot have timeline_title without timeline_id', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -986,17 +1179,55 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', timeline_title: 'some-title', }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); }); + test('The default for "from" will be "now-6m"', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).value.from + ).toEqual('now-6m'); + }); + + test('The default for "to" will be "now"', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).value.to + ).toEqual('now'); + }); + test('You cannot set the severity to a value other than low, medium, high, or critical', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', risk_score: 50, description: 'some description', name: 'some-name', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index 3aa8e007a8cbd..3e5a608d6b657 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -37,31 +37,44 @@ import { } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; + +/** + * This almost identical to the create_rules_schema except for a few details. + * - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented + * This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in + * this will update the rule's version number. + * - id is on here because you can pass in an id to update using it instead of rule_id. + */ export const updateRulesSchema = Joi.object({ - description, - enabled, - false_positives, + description: description.required(), + enabled: enabled.default(true), + id, + false_positives: false_positives.default([]), filters, - from, + from: from.default('now-6m'), rule_id, - id, index, - interval, - query: query.allow(''), - language, + interval: interval.default('5m'), + query: query.allow('').default(''), + language: language.default('kuery'), output_index, - saved_id, + saved_id: saved_id.when('type', { + is: 'saved_query', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), timeline_id, timeline_title, meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - references, + risk_score: risk_score.required(), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), + name: name.required(), + severity: severity.required(), + tags: tags.default([]), + to: to.default('now'), + type: type.required(), + threat: threat.default([]), + references: references.default([]), version, }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index beef8b4199c15..cc02a6a02962a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -36,7 +36,13 @@ export const createReadTagsRoute: Hapi.ServerRoute = { }); return tags; } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index ffd0c791c5bb6..3e3ccfe5babef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -18,51 +18,69 @@ import { describe('utils', () => { describe('transformError', () => { - test('returns boom if it is a boom object', () => { - const boom = new Boom(''); + test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { + const boom = new Boom('some boom message'); const transformed = transformError(boom); - expect(transformed).toBe(boom); + expect(transformed).toEqual({ + message: 'An internal server error occurred', + statusCode: 500, + }); }); - test('returns a boom if it is some non boom object that has a statusCode', () => { + test('returns transformed output if it is some non boom object that has a statusCode', () => { const error: Error & { statusCode?: number } = { statusCode: 403, name: 'some name', message: 'some message', }; const transformed = transformError(error); - expect(Boom.isBoom(transformed)).toBe(true); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); }); - test('returns a boom with the message set', () => { + test('returns a transformed message with the message set and statusCode', () => { const error: Error & { statusCode?: number } = { statusCode: 403, name: 'some name', message: 'some message', }; const transformed = transformError(error); - expect(transformed.message).toBe('some message'); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); }); - test('does not return a boom if it is some non boom object but it does not have a status Code.', () => { + test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { const error: Error = { name: 'some name', message: 'some message', }; const transformed = transformError(error); - expect(Boom.isBoom(transformed)).toBe(false); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 500, + }); }); - test('it detects a TypeError and returns a Boom', () => { + test('it detects a TypeError and returns a status code of 400 from that particular error type', () => { const error: TypeError = new TypeError('I have a type error'); const transformed = transformError(error); - expect(Boom.isBoom(transformed)).toBe(true); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); }); test('it detects a TypeError and returns a Boom status of 400', () => { const error: TypeError = new TypeError('I have a type error'); - const transformed = transformError(error) as Boom; - expect(transformed.output.statusCode).toBe(400); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 416c76b5d4eb5..af78f60f16ae4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -8,20 +8,37 @@ import Boom from 'boom'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../types'; -export const transformError = (err: Error & { statusCode?: number }) => { +export interface OutputError { + message: string; + statusCode: number; +} + +export const transformError = (err: Error & { statusCode?: number }): OutputError => { if (Boom.isBoom(err)) { - return err; + return { + message: err.output.payload.message, + statusCode: err.output.statusCode, + }; } else { if (err.statusCode != null) { - return new Boom(err.message, { statusCode: err.statusCode }); + return { + message: err.message, + statusCode: err.statusCode, + }; } else if (err instanceof TypeError) { // allows us to throw type errors instead of booms in some conditions // where we don't want to mingle Boom with the rest of the code - return new Boom(err.message, { statusCode: 400 }); + return { + message: err.message, + statusCode: 400, + }; } else { // natively return the err and allow the regular framework // to deal with the error when it is a non Boom - return err; + return { + message: err.message ?? '(unknown error message)', + statusCode: 500, + }; } } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 82fe16882882e..ae28e2d35d1ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Alert } from '../../../../../alerting/server/types'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; @@ -37,7 +38,7 @@ export const createRules = ({ type, references, version, -}: CreateRuleParams) => { +}: CreateRuleParams): Promise => { return alertsClient.create({ data: { name, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts new file mode 100644 index 0000000000000..f560b67cdc587 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -0,0 +1,151 @@ +/* + * 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 { defaults } from 'lodash/fp'; +import { PartialAlert } from '../../../../../alerting/server/types'; +import { readRules } from './read_rules'; +import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { addTags } from './add_tags'; +import { ruleStatusSavedObjectType } from './saved_object_mappings'; +import { calculateVersion, calculateName, calculateInterval } from './utils'; + +export const patchRules = async ({ + alertsClient, + actionsClient, // TODO: Use this whenever we add feature support for different action types + savedObjectsClient, + description, + falsePositives, + enabled, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + from, + immutable, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + version, +}: PatchRuleParams): Promise => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { + return null; + } + + const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { + description, + falsePositives, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + from, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + version, + }); + + const nextParams = defaults( + { + ...rule.params, + }, + { + description, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + index, + maxSignals, + riskScore, + severity, + threat, + to, + type, + references, + version: calculatedVersion, + } + ); + + const update = await alertsClient.update({ + id: rule.id, + data: { + tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), + name: calculateName({ updatedName: name, originalName: rule.name }), + schedule: { + interval: calculateInterval(interval, rule.schedule.interval), + }, + actions: rule.actions, + params: nextParams, + }, + }); + + if (rule.enabled && enabled === false) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled === true) { + await alertsClient.enable({ id: rule.id }); + const ruleCurrentStatus = savedObjectsClient + ? await savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }) + : null; + // set current status for this rule to be 'going to run' + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = 'going to run'; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } + } else { + // enabled is null or undefined and we do not touch the rule + } + + if (enabled != null) { + return { ...update, enabled }; + } else { + return update; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index c15a08c0871d9..95466b5968f2a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -20,7 +20,12 @@ import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; import { RequestFacade } from '../../../types'; import { Alert } from '../../../../../alerting/server/types'; -export type UpdateRuleAlertParamsRest = Partial & { +export type PatchRuleAlertParamsRest = Partial & { + id: string | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; +}; + +export type UpdateRuleAlertParamsRest = RuleAlertParamsRest & { id: string | undefined; rule_id: RuleAlertParams['ruleId'] | undefined; }; @@ -34,6 +39,14 @@ export interface FindParamsRest { filter: string; } +export interface PatchRulesRequest extends RequestFacade { + payload: PatchRuleAlertParamsRest; +} + +export interface BulkPatchRulesRequest extends RequestFacade { + payload: PatchRuleAlertParamsRest[]; +} + export interface UpdateRulesRequest extends RequestFacade { payload: UpdateRuleAlertParamsRest; } @@ -153,7 +166,12 @@ export interface Clients { actionsClient: ActionsClient; } -export type UpdateRuleParams = Partial & { +export type PatchRuleParams = Partial & { + id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; +} & Clients; + +export type UpdateRuleParams = RuleAlertParams & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 9e4b3a2e54806..b4d98b5354fb6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; -import { updateRules } from './update_rules'; +import { patchRules } from './patch_rules'; import { PrepackagedRules } from '../types'; export const updatePrepackagedRules = async ( @@ -45,7 +45,7 @@ export const updatePrepackagedRules = async ( // Note: we do not pass down enabled as we do not want to suddenly disable // or enable rules on the user when they were not expecting it if a rule updates - return updateRules({ + return patchRules({ alertsClient, actionsClient, description, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 634c0d5a52cb1..1dc5d8429fab8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -4,79 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaults, pickBy, isEmpty } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; -import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; import { addTags } from './add_tags'; import { ruleStatusSavedObjectType } from './saved_object_mappings'; - -export const calculateInterval = ( - interval: string | undefined, - ruleInterval: string | undefined -): string => { - if (interval != null) { - return interval; - } else if (ruleInterval != null) { - return ruleInterval; - } else { - return '5m'; - } -}; - -export const calculateVersion = ( - immutable: boolean, - currentVersion: number, - updateProperties: Partial> -): number => { - // early return if we are pre-packaged/immutable rule to be safe. We are never responsible - // for changing the version number of an immutable. Immutables are only responsible for changing - // their own version number. This would be really bad if an immutable version number is bumped by us - // due to a bug, hence the extra check and early bail if that is detected. - if (immutable === true) { - if (updateProperties.version != null) { - // we are an immutable rule but we are asking to update the version number so go ahead - // and update it to what is asked. - return updateProperties.version; - } else { - // we are immutable and not asking to update the version number so return the existing version - return currentVersion; - } - } - - // white list all properties but the enabled/disabled flag. We don't want to auto-increment - // the version number if only the enabled/disabled flag is being set. Likewise if we get other - // properties we are not expecting such as updatedAt we do not to cause a version number bump - // on that either. - const removedNullValues = pickBy( - (value: unknown) => value != null, - updateProperties - ); - if (isEmpty(removedNullValues)) { - return currentVersion; - } else { - return currentVersion + 1; - } -}; - -export const calculateName = ({ - updatedName, - originalName, -}: { - updatedName: string | undefined; - originalName: string | undefined; -}): string => { - if (updatedName != null) { - return updatedName; - } else if (originalName != null) { - return originalName; - } else { - // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a rule name became null or undefined at - // some point since TypeScript allows it. - return 'untitled'; - } -}; +import { calculateVersion } from './utils'; export const updateRules = async ({ alertsClient, @@ -141,47 +74,40 @@ export const updateRules = async ({ version, }); - const nextParams = defaults( - { - ...rule.params, - }, - { - description, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - index, - maxSignals, - riskScore, - severity, - threat, - to, - type, - references, - version: calculatedVersion, - } - ); - const update = await alertsClient.update({ id: rule.id, data: { - tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), - name: calculateName({ updatedName: name, originalName: rule.name }), - schedule: { - interval: calculateInterval(interval, rule.schedule.interval), - }, + tags: addTags(tags, rule.params.ruleId, immutable), + name, + schedule: { interval }, actions: rule.actions, - params: nextParams, + params: { + description, + ruleId: rule.params.ruleId, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + index, + maxSignals, + riskScore, + severity, + threat, + to, + type, + references, + version: calculatedVersion, + }, }, }); + if (rule.enabled && enabled === false) { await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { @@ -204,13 +130,7 @@ export const updateRules = async ({ ...currentStatusToDisable.attributes, }); } - } else { - // enabled is null or undefined and we do not touch the rule } - if (enabled != null) { - return { ...update, enabled }; - } else { - return update; - } + return { ...update, enabled }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts index 0d426fb03bd37..b7c36b20f44be 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateName, calculateVersion } from './update_rules'; -import { UpdateRuleParams } from './types'; +import { calculateInterval, calculateVersion, calculateName } from './utils'; +import { PatchRuleParams } from './types'; -describe('update_rules', () => { +describe('utils', () => { describe('#calculateInterval', () => { test('given a undefined interval, it returns the ruleInterval ', () => { const interval = calculateInterval(undefined, '10m'); @@ -44,7 +44,7 @@ describe('update_rules', () => { test('returning an updated version number if not given an immutable but an updated falsy value', () => { expect( - calculateVersion(false, 1, ({ description: false } as unknown) as UpdateRuleParams) + calculateVersion(false, 1, ({ description: false } as unknown) as PatchRuleParams) ).toEqual(2); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts new file mode 100644 index 0000000000000..7d6091f6b97fa --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts @@ -0,0 +1,75 @@ +/* + * 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 { pickBy, isEmpty } from 'lodash/fp'; +import { PatchRuleParams } from './types'; + +export const calculateInterval = ( + interval: string | undefined, + ruleInterval: string | undefined +): string => { + if (interval != null) { + return interval; + } else if (ruleInterval != null) { + return ruleInterval; + } else { + return '5m'; + } +}; + +export const calculateVersion = ( + immutable: boolean, + currentVersion: number, + updateProperties: Partial> +): number => { + // early return if we are pre-packaged/immutable rule to be safe. We are never responsible + // for changing the version number of an immutable. Immutables are only responsible for changing + // their own version number. This would be really bad if an immutable version number is bumped by us + // due to a bug, hence the extra check and early bail if that is detected. + if (immutable === true) { + if (updateProperties.version != null) { + // we are an immutable rule but we are asking to update the version number so go ahead + // and update it to what is asked. + return updateProperties.version; + } else { + // we are immutable and not asking to update the version number so return the existing version + return currentVersion; + } + } + + // white list all properties but the enabled/disabled flag. We don't want to auto-increment + // the version number if only the enabled/disabled flag is being set. Likewise if we get other + // properties we are not expecting such as updatedAt we do not to cause a version number bump + // on that either. + const removedNullValues = pickBy( + (value: unknown) => value != null, + updateProperties + ); + if (isEmpty(removedNullValues)) { + return currentVersion; + } else { + return currentVersion + 1; + } +}; + +export const calculateName = ({ + updatedName, + originalName, +}: { + updatedName: string | undefined; + originalName: string | undefined; +}): string => { + if (updatedName != null) { + return updatedName; + } else if (originalName != null) { + return originalName; + } else { + // You really should never get to this point. This is a fail safe way to send back + // the name of "untitled" just in case a rule name became null or undefined at + // some point since TypeScript allows it. + return 'untitled'; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh new file mode 100755 index 0000000000000..8094d9bad552c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh @@ -0,0 +1,31 @@ +#!/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 +RULES=(${@:-./rules/patches/simplest_updated_name.json}) + +# Example: ./patch_rule.sh +# Example: ./patch_rule.sh ./rules/patches/simplest_updated_name.json +# Example glob: ./patch_rule.sh ./rules/patches/* +for RULE in "${RULES[@]}" +do { + [ -e "$RULE" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ + -d @${RULE} \ + | jq .; +} & +done + +wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh new file mode 100755 index 0000000000000..3ae32445433ad --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh @@ -0,0 +1,22 @@ +#!/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 +RULES=${1:-./rules/bulk/patch_names.json} + +# Example: ./patch_rule_bulk.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_update \ + -d @${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json index 9e5328ffabe2e..ef172acde3807 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json @@ -2,6 +2,7 @@ { "name": "Simplest Query Number 1", "description": "Simplest query with the least amount of fields required", + "rule_id": "query-rule-id-1", "risk_score": 1, "severity": "high", "type": "query", @@ -12,6 +13,7 @@ { "name": "Simplest Query Number 2", "description": "Simplest query with the least amount of fields required", + "rule_id": "query-rule-id-2", "risk_score": 2, "severity": "low", "type": "query", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md new file mode 100644 index 0000000000000..bb47e4adfc56d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md @@ -0,0 +1,25 @@ +These are example PATCH rules to see how to patch various parts of the rules. +You either have to use the id, or you have to use the rule_id in order to patch +the rules. rule_id acts as an external_id where you can patch rules across different +Kibana systems where id acts as a normal server generated id which is not normally shared +across different Kibana systems. + +The only thing you cannot patch is the `rule_id` or regular `id` of the system. If `rule_id` +is incorrect then you have to delete the rule completely and re-initialize it with the +correct `rule_id` + +First add all the examples from queries like so: + +```sh +./post_rule.sh ./rules/queries/*.json +``` + +Then to selectively patch a rule add the file of your choosing to patch: + +```sh +./patch_rule.sh ./rules/patches/.json +``` + +Take note that the ones with "id" must be changed to a GUID that only you know about through +a `./find_rules.sh`. For example to grab a GUID id off of the first found record that exists +you can do: `./find_rules.sh | jq '.data[0].id'` and then replace the id in `patches/simplest_update_risk_score_by_id.json` with that particular id to watch it happen. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json new file mode 100644 index 0000000000000..a94558143882b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "enabled": false +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json new file mode 100644 index 0000000000000..bfe7c7f546fc3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "enabled": true +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json new file mode 100644 index 0000000000000..00966ddba7c7a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json @@ -0,0 +1,4 @@ +{ + "id": "ade31ba8-dc49-4c18-b7f4-370b35df5f57", + "risk_score": 38 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json new file mode 100644 index 0000000000000..ad3c78183297d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "risk_score": 98 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json new file mode 100644 index 0000000000000..56c9f151dc712 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "name": "Changes only the name to this new value" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json new file mode 100644 index 0000000000000..72a535f0ef641 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "interval": "6m" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json new file mode 100644 index 0000000000000..eb210cd8153d7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json @@ -0,0 +1,82 @@ +{ + "name": "Updates a query with all possible fields that can be updated", + "description": "Kitchen Sink (everything) query that has all possible fields filled out.", + "false_positives": [ + "https://www.example.com/some-article-about-a-false-positive", + "some text string about why another condition could be a false positive" + ], + "rule_id": "rule-id-everything", + "filters": [ + { + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + } + } + ], + "enabled": false, + "index": ["auditbeat-*", "filebeat-*"], + "interval": "5m", + "query": "user.name: root or user.name: admin", + "output_index": ".siem-signals-default", + "meta": { + "anything_you_want_ui_related_or_otherwise": { + "as_deep_structured_as_you_need": { + "any_data_type": {} + } + } + }, + "language": "kuery", + "risk_score": 1, + "max_signals": 100, + "tags": ["tag 1", "tag 2", "any tag you want"], + "to": "now", + "from": "now-6m", + "severity": "high", + "type": "query", + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0040", + "name": "impact", + "reference": "https://attack.mitre.org/tactics/TA0040/" + }, + "technique": [ + { + "id": "T1499", + "name": "endpoint denial of service", + "reference": "https://attack.mitre.org/techniques/T1499/" + } + ] + }, + { + "framework": "Some other Framework you want", + "tactic": { + "id": "some-other-id", + "name": "Some other name", + "reference": "https://example.com" + }, + "technique": [ + { + "id": "some-other-id", + "name": "some other technique name", + "reference": "https://example.com" + } + ] + } + ], + "references": [ + "http://www.example.com/some-article-about-attack", + "Some plain text string here explaining why this is a valid thing to look out for" + ], + "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title", + "version": 42 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json new file mode 100644 index 0000000000000..be833105792c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json @@ -0,0 +1,4 @@ +{ + "rule_id": "tags-query", + "tags": ["tag_3"] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json new file mode 100644 index 0000000000000..27dee7dd81463 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json @@ -0,0 +1,5 @@ +{ + "rule_id": "query-rule-id", + "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json new file mode 100644 index 0000000000000..8df63dd22bf9a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "version": 500 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md index 97a5d31bb0133..5fdf0faa122e9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md @@ -1,4 +1,4 @@ -These are example PUT rules to see how to update various parts of the rules. +These are example update rules to see how to update various parts of the rules. You either have to use the id, or you have to use the rule_id in order to update the rules. rule_id acts as an external_id where you can update rules across different Kibana systems where id acts as a normal server generated id which is not normally shared @@ -14,7 +14,7 @@ First add all the examples from queries like so: ./post_rule.sh ./rules/queries/*.json ``` -Then to selectively update a rule add the file of your choosing to update: +Then to selectively update a rule add the file of your choosing to patch: ```sh ./update_rule.sh ./rules/updates/.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json index a94558143882b..8752d66e4a0dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json @@ -1,4 +1,10 @@ { + "name": "Some new name", + "description": "Changing the name and disabling this query", "rule_id": "query-rule-id", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", "enabled": false } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json index bfe7c7f546fc3..3556e2c94da48 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json @@ -1,4 +1,10 @@ { + "name": "Some new name", + "description": "Changing the name and enabling this query", "rule_id": "query-rule-id", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", "enabled": true } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json index 00966ddba7c7a..847c7480ef6b5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json @@ -1,4 +1,9 @@ { - "id": "ade31ba8-dc49-4c18-b7f4-370b35df5f57", - "risk_score": 38 + "id": "1100ba1b-ed7e-4755-b326-1f6fa2bd6758", + "name": "Some new name", + "description": "Changing the name and changing the risk score", + "risk_score": 38, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json index ad3c78183297d..5c1e71e3833a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json @@ -1,4 +1,9 @@ { "rule_id": "query-rule-id", - "risk_score": 98 + "name": "Some new name", + "description": "Changing the name and changing the risk score", + "risk_score": 98, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json index 56c9f151dc712..ef086743e07f4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json @@ -1,4 +1,9 @@ { + "name": "Changes only the name to this new value", + "description": "Query with a rule_id that acts like an external id", "rule_id": "query-rule-id", - "name": "Changes only the name to this new value" + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json index 72a535f0ef641..80bf306fe36b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json @@ -1,4 +1,10 @@ { "rule_id": "query-rule-id", - "interval": "6m" + "interval": "6m", + "name": "Some new name", + "description": "Changing the interval and risk score", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 0 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json index be833105792c6..4b9f773a1a4b0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json @@ -1,4 +1,10 @@ { - "rule_id": "tags-query", - "tags": ["tag_3"] + "rule_id": "query-rule-id", + "tags": ["tag_1", "tag_2", "tag_3"], + "name": "Some new name", + "description": "Adding tags and a few other updates such as name", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 10 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json index 27dee7dd81463..0fb8626fe3ce4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json @@ -1,5 +1,11 @@ { "rule_id": "query-rule-id", "timeline_id": "other-timeline-id", - "timeline_title": "other-timeline-title" + "timeline_title": "other-timeline-title", + "name": "Some new name", + "description": "Adding tags and a few other updates such as name", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 10 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json index 8df63dd22bf9a..4df935fb3f6b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json @@ -1,4 +1,10 @@ { "rule_id": "query-rule-id", - "version": 500 + "version": 500, + "name": "Changes the version to arbitrary number", + "description": "Changes the version to some arbitrary number", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 10 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh index aa22db965664a..22bc4fb7bf584 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -14,7 +14,7 @@ RULES=(${@:-./rules/updates/simplest_updated_name.json}) # Example: ./update_rule.sh # Example: ./update_rule.sh ./rules/updates/simplest_updated_name.json -# Example glob: ./post_rule.sh ./rules/updates/* +# Example glob: ./update_rule.sh ./rules/updates/* for RULE in "${RULES[@]}" do { [ -e "$RULE" ] || continue diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh index c9cb0676821c5..11fb8d0b6f81c 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -RULES=${1:-./rules/bulk/update_names.json} +RULES=${1:-./rules/bulk/multiple_simplest_queries.json} # Example: ./update_rule_bulk.sh curl -s -k \ diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index e984c23385508..8ef87f2acc7a4 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -15,6 +15,7 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/api_integration/config.js'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), + require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.js'), require.resolve('../test/kerberos_api_integration/config'), require.resolve('../test/kerberos_api_integration/anonymous_access.config'), diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts new file mode 100644 index 0000000000000..3e626e945d7d8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -0,0 +1,92 @@ +/* + * 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 path from 'path'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.server-log', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +// eslint-disable-next-line import/no-default-export +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers, + services, + junit: { + reportName: 'X-Pack Detection Engine API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'aad')}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts @@ -0,0 +1,11 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/detection_engine_api_integration/common/services.ts b/x-pack/test/detection_engine_api_integration/common/services.ts new file mode 100644 index 0000000000000..a927a31469bab --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/common/services.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { services } from '../../api_integration/services'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts new file mode 100644 index 0000000000000..081b901c47fc3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts @@ -0,0 +1,14 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, +}); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts new file mode 100644 index 0000000000000..5e09013fb32a3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -0,0 +1,92 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('add_prepackaged_rules', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body).to.eql({ + message: + 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', + status_code: 400, + }); + }); + }); + + describe('creating prepackaged rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should contain two output keys of rules_installed and rules_updated', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql(['rules_installed', 'rules_updated']); + }); + + it('should create the prepackaged rules and return a count greater than zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_installed).to.be.greaterThan(0); + }); + + it('should create the prepackaged rules that the rules_updated is of size zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_updated).to.eql(0); + }); + + it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_installed).to.eql(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts new file mode 100644 index 0000000000000..d6a238e5b0940 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -0,0 +1,97 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('create_rules', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('creating rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should create a single rule without a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(409); + + expect(body).to.eql({ + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts new file mode 100644 index 0000000000000..dfa297c85dfb8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -0,0 +1,124 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('create_rules_bulk', () => { + describe('validation errors', () => { + it('should give a 200 even if the index does not exist as all bulks return a 200 but have an error of 409 bad request in the body', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); + }); + + describe('creating rules in bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should create a single rule without a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRuleWithoutRuleId()]) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + // TODO: This is a valid issue and will be fixed in an upcoming PR and then enabled once that PR is merged + it.skip('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule(), getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: 'Conflict', + message: 'rule_id: "rule-1" already exists', + statusCode: 409, + }, + ]); + }); + + it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id that already exists', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'foo') + .send([getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts new file mode 100644 index 0000000000000..ee34e5e261987 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -0,0 +1,123 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('delete_rules', () => { + describe('deleting rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // create a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // delete the rule by its rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule where the rule_id is auto-generated + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its auto-generated rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=${bodyWithCreatedRule.rule_id}`) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its auto-generated id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=${bodyWithCreatedRule.id}`) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .set('kbn-xsrf', 'true') + .query() + .expect(404); + + expect(body).to.eql({ + message: 'id: "fake_id" not found', + status_code: 404, + }); + }); + + it('should return an error if the rule_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`) + .set('kbn-xsrf', 'true') + .query() + .expect(404); + + expect(body).to.eql({ + message: 'rule_id: "fake_id" not found', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts new file mode 100644 index 0000000000000..5a1c178f6b211 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -0,0 +1,293 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('delete_rules_bulk', () => { + describe('deleting rules bulk using DELETE', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // add a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete the rule in bulk + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1' }]) + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule without a rule_id + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: bodyWithCreatedRule.rule_id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should return an error if the id does not exist when trying to delete an id', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO This is a known issue where it should be id and not rule_id + }, + ]); + }); + + it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => { + // add the rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + getSimpleRuleOutputWithoutRuleId(), + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + }); + + // This is a repeat of the tests above but just using POST instead of DELETE + describe('deleting rules bulk using POST', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // add a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'foo') + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete the rule in bulk + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1' }]) + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule without a rule_id + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its rule_id + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: bodyWithCreatedRule.rule_id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its id + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should return an error if the id does not exist when trying to delete an id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO This is a known issue where it should be id and not rule_id + }, + ]); + }); + + it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => { + // add the rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + getSimpleRuleOutputWithoutRuleId(), + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts new file mode 100644 index 0000000000000..8882448dfcdc2 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts @@ -0,0 +1,134 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + binaryToString, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('export_rules', () => { + describe('exporting rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should set the response content types to be expected', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .expect('Content-Type', 'application/ndjson') + .expect('Content-Disposition', 'attachment; filename="export.ndjson"'); + }); + + it('should export a single rule with a rule_id', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .parse(binaryToString); + + const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[0]); + const bodyToTest = removeServerGeneratedProperties(bodySplitAndParsed); + + expect(bodyToTest).to.eql(getSimpleRuleOutput()); + }); + + it('should export a exported count with a single rule_id', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .parse(binaryToString); + + const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]); + + expect(bodySplitAndParsed).to.eql({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + }); + }); + + it('should export exactly two rules given two rules', async () => { + // post rule 1 + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // post rule 2 + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .parse(binaryToString); + + const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); + const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); + const firstRule = removeServerGeneratedProperties(firstRuleParsed); + const secondRule = removeServerGeneratedProperties(secondRuleParsed); + + expect([firstRule, secondRule]).to.eql([ + getSimpleRuleOutput('rule-2'), + getSimpleRuleOutput('rule-1'), + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts new file mode 100644 index 0000000000000..82e506b23ca97 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts @@ -0,0 +1,98 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getComplexRule, + getComplexRuleOutput, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('find_rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should return an empty find body correctly if no rules are loaded', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + perPage: 20, + total: 0, + }); + }); + + it('should return a single rule when a single rule is loaded from a find with defaults added', async () => { + // add a single rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // query the single rule from _find + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send(); + + body.data = [removeServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getSimpleRuleOutput()], + page: 1, + perPage: 20, + total: 1, + }); + }); + + it('should return a single rule when a single rule is loaded from a find with everything for the rule added', async () => { + // add a single rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getComplexRule()) + .expect(200); + + // query and expect that we get back one record in the find + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send(); + + body.data = [removeServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getComplexRuleOutput()], + page: 1, + perPage: 20, + total: 1, + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts new file mode 100644 index 0000000000000..49cf150126fda --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts @@ -0,0 +1,100 @@ +/* + * 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 expect from '@kbn/expect'; + +import { + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_URL, +} from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, getSimpleRule } from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_prepackaged_rules_status', () => { + describe('getting prepackaged rules status', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should return expected JSON keys of the pre-packaged rules status', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql([ + 'rules_custom_installed', + 'rules_installed', + 'rules_not_installed', + 'rules_not_updated', + ]); + }); + + it('should return that rules_not_installed are greater than zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_not_installed).to.be.greaterThan(0); + }); + + it('should return that rules_custom_installed, rules_installed, and rules_not_updated are zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_custom_installed).to.eql(0); + expect(body.rules_installed).to.eql(0); + expect(body.rules_not_updated).to.eql(0); + }); + + it('should show that one custom rule is installed when a custom rule is added', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_custom_installed).to.eql(1); + expect(body.rules_installed).to.eql(0); + expect(body.rules_not_updated).to.eql(0); + }); + + it('should show rules are installed when adding pre-packaged rules', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_installed).to.be.greaterThan(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts new file mode 100644 index 0000000000000..e8fd1e4298c22 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -0,0 +1,331 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleAsNdjson, + getSimpleRuleOutput, + removeServerGeneratedProperties, + ruleToNdjson, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('import_rules', () => { + describe('importing rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should set the response content types to be expected', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + }); + + it('should reject with an error if the file type is not that of a ndjson', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.txt') + .query() + .expect(400); + + expect(body).to.eql({ + status_code: 400, + message: 'Invalid file extension .txt', + }); + }); + + it('should report that it imported a simple rule successfully', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should be able to read an imported rule back out correctly', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1')); + }); + + it('should be able to import two rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 2, + }); + }); + + it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], // TODO: This should have a conflict within it as an error rather than an empty array + success: true, + success_count: 1, + }); + }); + + it('should NOT report a conflict if there is an attempt to import two rules with the same rule_id and overwrite is set to true', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 0, + }); + }); + + it('should NOT report a conflict if there is an attempt to import a rule with a rule_id that already exists and overwrite is set to true', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should overwrite an existing rule if overwrite is set to true', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const simpleRule = getSimpleRule('rule-1'); + simpleRule.name = 'some other name'; + const ndjson = ruleToNdjson(simpleRule); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', ndjson, 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + const ruleOutput = getSimpleRuleOutput('rule-1'); + ruleOutput.name = 'some other name'; + ruleOutput.version = 2; + expect(bodyToCompare).to.eql(ruleOutput); + }); + + it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 2, + }); + }); + + it('should report a mix of conflicts and a mix of successes', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'rule_id: "rule-2" already exists', + status_code: 409, + }, + rule_id: 'rule-2', + }, + ], + success: false, + success_count: 1, + }); + }); + + it('should be able to correctly read back a mixed import of different rules even if some cause conflicts', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .query() + .expect(200); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .query() + .expect(200); + + const { body: bodyOfRule1 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const { body: bodyOfRule2 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-2`) + .send() + .expect(200); + + const { body: bodyOfRule3 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-3`) + .send() + .expect(200); + + const bodyToCompareOfRule1 = removeServerGeneratedProperties(bodyOfRule1); + const bodyToCompareOfRule2 = removeServerGeneratedProperties(bodyOfRule2); + const bodyToCompareOfRule3 = removeServerGeneratedProperties(bodyOfRule3); + + expect([bodyToCompareOfRule1, bodyToCompareOfRule2, bodyToCompareOfRule3]).to.eql([ + getSimpleRuleOutput('rule-1'), + getSimpleRuleOutput('rule-2'), + getSimpleRuleOutput('rule-3'), + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts new file mode 100644 index 0000000000000..ca6ef5b6cede9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled', function() { + this.tags('ciGroup1'); + + loadTestFile(require.resolve('./add_prepackaged_rules')); + loadTestFile(require.resolve('./create_rules')); + loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./delete_rules')); + loadTestFile(require.resolve('./delete_rules_bulk')); + loadTestFile(require.resolve('./export_rules')); + loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./get_prepackaged_rules_status')); + loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./update_rules')); + loadTestFile(require.resolve('./update_rules_bulk')); + loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./patch_rules')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts new file mode 100644 index 0000000000000..53a3d15690efc --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -0,0 +1,212 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleOutputWithoutRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('patch_rules', () => { + describe('patch rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should patch a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch a single rule property of name using the auto-generated rule_id', async () => { + // create a simple rule + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: createRuleBody.rule_id, name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutputWithoutRuleId(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ id: createdBody.id, name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change the version of a rule when it patches only enabled', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', enabled: false }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it patches enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false and another property + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', severity: 'low', enabled: false }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change other properties when it does patches', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's timeline_title + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.timeline_title = 'some title'; + outputRule.timeline_id = 'some id'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should give a 404 if it is given a fake id', async () => { + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ id: 'fake_id', name: 'some other name' }) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should give a 404 if it is given a fake rule_id', async () => { + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'fake_id', name: 'some other name' }) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts new file mode 100644 index 0000000000000..3d14bc2db47b4 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -0,0 +1,356 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + getSimpleRuleOutputWithoutRuleId, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('patch_rules_bulk', () => { + describe('patch rules bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should patch a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch two rule properties of name using the two rules rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // patch both rule names + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { rule_id: 'rule-1', name: 'some other name' }, + { rule_id: 'rule-2', name: 'some other name' }, + ]) + .expect(200); + + const outputRule1 = getSimpleRuleOutput(); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutput('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should patch a single rule property of name using an id', async () => { + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: createRuleBody.id, name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch two rule properties of name using the two rules id', async () => { + // create a simple rule + const { body: createRule1 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + const { body: createRule2 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // patch both rule names + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { id: createRule1.id, name: 'some other name' }, + { id: createRule2.id, name: 'some other name' }, + ]) + .expect(200); + + const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should patch a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: createdBody.id, name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change the version of a rule when it patches only enabled', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', enabled: false }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it patches enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false and another property + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', severity: 'low', enabled: false }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change other properties when it does patches', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's timeline_title + await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }]) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.timeline_title = 'some title'; + outputRule.timeline_id = 'some id'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: 'fake_id', name: 'some other name' }]) + .expect(200); + + expect(body).to.eql([ + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'fake_id', name: 'some other name' }]) + .expect(200); + + expect(body).to.eql([ + { + rule_id: 'fake_id', + error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, + }, + ]); + }); + + it('should patch one rule property and give an error about a second fake rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch one rule name and give a fake id for the second + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { rule_id: 'rule-1', name: 'some other name' }, + { rule_id: 'fake_id', name: 'some other name' }, + ]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should patch one rule property and give an error about a second fake id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch one rule name and give a fake id for the second + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { id: createdBody.id, name: 'some other name' }, + { id: 'fake_id', name: 'some other name' }, + ]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO: This should be id and not rule_id in the codebase + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts new file mode 100644 index 0000000000000..2ea62b0756f73 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts @@ -0,0 +1,120 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('read_rules', () => { + describe('reading rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should be able to read a single rule using rule_id', async () => { + // create a simple rule to read + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should be able to read a single rule using id', async () => { + // create a simple rule to read + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should be able to read a single rule with an auto-generated rule_id', async () => { + // create a simple rule to read + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${createRuleBody.rule_id}`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should return 404 if given a fake rule_id', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts new file mode 100644 index 0000000000000..92c78be72bf01 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -0,0 +1,221 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleOutputWithoutRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('update_rules', () => { + describe('update rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should update a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.rule_id = 'rule-1'; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update a single rule property of name using an auto-generated rule_id', async () => { + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.rule_id = createRuleBody.rule_id; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutputWithoutRuleId(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.name = 'some other name'; + updatedRule.id = createdBody.id; + delete updatedRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it updates enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's enabled to false and another property + const updatedRule = getSimpleRule('rule-1'); + updatedRule.severity = 'low'; + updatedRule.enabled = false; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.timeline_title = 'some title'; + ruleUpdate.timeline_id = 'some id'; + + // update a simple rule's timeline_title + await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate) + .expect(200); + + const ruleUpdate2 = getSimpleRule('rule-1'); + ruleUpdate2.name = 'some other name'; + + // update a simple rule's name + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate2) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should give a 404 if it is given a fake id', async () => { + const simpleRule = getSimpleRule(); + simpleRule.id = 'fake_id'; + delete simpleRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should give a 404 if it is given a fake rule_id', async () => { + const simpleRule = getSimpleRule(); + simpleRule.rule_id = 'fake_id'; + delete simpleRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts new file mode 100644 index 0000000000000..4894cac2b2608 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -0,0 +1,386 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + getSimpleRuleOutputWithoutRuleId, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('update_rules_bulk', () => { + describe('update rules bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should update a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const updatedRule = getSimpleRule('rule-1'); + updatedRule.name = 'some other name'; + + // update a simple rule's name + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update two rule properties of name using the two rules rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.name = 'some other name'; + + const updatedRule2 = getSimpleRule('rule-2'); + updatedRule2.name = 'some other name'; + + // update both rule names + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1, updatedRule2]) + .expect(200); + + const outputRule1 = getSimpleRuleOutput(); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutput('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should update a single rule property of name using an id', async () => { + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createRuleBody.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update two rule properties of name using the two rules id', async () => { + // create a simple rule + const { body: createRule1 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + const { body: createRule2 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // update both rule names + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createRule1.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const updatedRule2 = getSimpleRule('rule-1'); + updatedRule2.id = createRule2.id; + updatedRule2.name = 'some other name'; + delete updatedRule2.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1, updatedRule2]) + .expect(200); + + const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should update a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createdBody.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it updates enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's enabled to false and another property + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.severity = 'low'; + updatedRule1.enabled = false; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's timeline_title + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.timeline_title = 'some title'; + ruleUpdate.timeline_id = 'some id'; + + await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + // update a simple rule's name + const ruleUpdate2 = getSimpleRule('rule-1'); + ruleUpdate2.name = 'some other name'; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.id = 'fake_id'; + delete ruleUpdate.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + expect(body).to.eql([ + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.rule_id = 'fake_id'; + delete ruleUpdate.id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + expect(body).to.eql([ + { + rule_id: 'fake_id', + error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, + }, + ]); + }); + + it('should update one rule property and give an error about a second fake rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.name = 'some other name'; + delete ruleUpdate.id; + + const ruleUpdate2 = getSimpleRule('fake_id'); + ruleUpdate2.name = 'some other name'; + delete ruleUpdate.id; + + // update one rule name and give a fake id for the second + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate, ruleUpdate2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should update one rule property and give an error about a second fake id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update one rule name and give a fake id for the second + const rule1 = getSimpleRule(); + delete rule1.rule_id; + rule1.id = createdBody.id; + rule1.name = 'some other name'; + + const rule2 = getSimpleRule(); + delete rule2.rule_id; + rule2.id = 'fake_id'; + rule2.name = 'some other name'; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([rule1, rule2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO: This should be id and not rule_id in the codebase + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts new file mode 100644 index 0000000000000..b78073c0e737b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -0,0 +1,345 @@ +/* + * 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 { OutputRuleAlertRest } from '../../../../legacy/plugins/siem/server/lib/detection_engine/types'; +import { DETECTION_ENGINE_INDEX_URL } from '../../../../legacy/plugins/siem/common/constants'; + +/** + * This will remove server generated properties such as date times, etc... + * @param rule Rule to pass in to remove typical server generated properties + */ +export const removeServerGeneratedProperties = ( + rule: Partial +): Partial => { + const { + created_at, + updated_at, + id, + last_success_at, + last_success_message, + status, + status_date, + ...removedProperties + } = rule; + return removedProperties; +}; + +/** + * This will remove server generated properties such as date times, etc... including the rule_id + * @param rule Rule to pass in to remove typical server generated properties + */ +export const removeServerGeneratedPropertiesIncludingRuleId = ( + rule: Partial +): Partial => { + const ruleWithRemovedProperties = removeServerGeneratedProperties(rule); + const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties; + return additionalRuledIdRemoved; +}; + +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + rule_id: ruleId, + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', +}); + +/** + * This is a typical simple rule for testing that is easy for most basic testing + */ +export const getSimpleRuleWithoutRuleId = (): Partial => { + const simpleRule = getSimpleRule(); + const { rule_id, ...ruleWithoutId } = simpleRule; + return ruleWithoutId; +}; + +/** + * Useful for export_api testing to convert from a multi-part binary back to a string + * @param res Response + * @param callback Callback + */ +export const binaryToString = (res: any, callback: any): void => { + res.setEncoding('binary'); + res.data = ''; + res.on('data', (chunk: any) => { + res.data += chunk; + }); + res.on('end', () => { + callback(null, Buffer.from(res.data)); + }); +}; + +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults. + */ +export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => ({ + created_by: 'elastic', + description: 'Simple Rule Query', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: ruleId, + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 1, + name: 'Simple Rule Query', + query: 'user.name: root or user.name: admin', + references: [], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, +}); + +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults. + */ +export const getSimpleRuleOutputWithoutRuleId = ( + ruleId = 'rule-1' +): Partial => { + const rule = getSimpleRuleOutput(ruleId); + const { rule_id, ...ruleWithoutRuleId } = rule; + return ruleWithoutRuleId; +}; + +/** + * Remove all alerts from the .kibana index + * @param es The ElasticSearch handle + */ +export const deleteAllAlerts = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:alert', + waitForCompletion: true, + refresh: 'wait_for', + }); +}; + +/** + * Creates the signals index for use inside of beforeEach blocks of tests + * @param supertest The supertest client library + */ +export const createSignalsIndex = async (supertest: any): Promise => { + await supertest + .post(DETECTION_ENGINE_INDEX_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); +}; + +/** + * Deletes the signals index for use inside of afterEach blocks of tests + * @param supertest The supertest client library + */ +export const deleteSignalsIndex = async (supertest: any): Promise => { + await supertest + .delete(DETECTION_ENGINE_INDEX_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); +}; + +/** + * Given an array of rule_id strings this will return a ndjson buffer which is useful + * for testing uploads. + * @param ruleIds Array of strings of rule_ids + */ +export const getSimpleRuleAsNdjson = (ruleIds: string[]): Buffer => { + const stringOfRules = ruleIds.map(ruleId => { + const simpleRule = getSimpleRule(ruleId); + return JSON.stringify(simpleRule); + }); + return Buffer.from(stringOfRules.join('\n')); +}; + +/** + * Given a rule this will convert it to an ndjson buffer which is useful for + * testing upload features. + * @param rule The rule to convert to ndjson + */ +export const ruleToNdjson = (rule: Partial): Buffer => { + const stringified = JSON.stringify(rule); + return Buffer.from(`${stringified}\n`); +}; + +/** + * This will return a complex rule with all the outputs possible + * @param ruleId The ruleId to set which is optional and defaults to rule-1 + */ +export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Complex Rule Query', + description: 'Complex Rule Query', + false_positives: [ + 'https://www.example.com/some-article-about-a-false-positive', + 'some text string about why another condition could be a false positive', + ], + risk_score: 1, + rule_id: ruleId, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + enabled: false, + index: ['auditbeat-*', 'filebeat-*'], + interval: '5m', + output_index: '.siem-signals-default', + meta: { + anything_you_want_ui_related_or_otherwise: { + as_deep_structured_as_you_need: { + any_data_type: {}, + }, + }, + }, + max_signals: 10, + tags: ['tag 1', 'tag 2', 'any tag you want'], + to: 'now', + from: 'now-6m', + severity: 'high', + language: 'kuery', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + { + framework: 'Some other Framework you want', + tactic: { + id: 'some-other-id', + name: 'Some other name', + reference: 'https://example.com', + }, + technique: [ + { + id: 'some-other-id', + name: 'some other technique name', + reference: 'https://example.com', + }, + ], + }, + ], + references: [ + 'http://www.example.com/some-article-about-attack', + 'Some plain text string here explaining why this is a valid thing to look out for', + ], + timeline_id: 'timeline_id', + timeline_title: 'timeline_title', + version: 1, + query: 'user.name: root or user.name: admin', +}); + +/** + * This will return a complex rule with all the outputs possible + * @param ruleId The ruleId to set which is optional and defaults to rule-1 + */ +export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ + created_by: 'elastic', + name: 'Complex Rule Query', + description: 'Complex Rule Query', + false_positives: [ + 'https://www.example.com/some-article-about-a-false-positive', + 'some text string about why another condition could be a false positive', + ], + risk_score: 1, + rule_id: ruleId, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + enabled: false, + index: ['auditbeat-*', 'filebeat-*'], + immutable: false, + interval: '5m', + output_index: '.siem-signals-default', + meta: { + anything_you_want_ui_related_or_otherwise: { + as_deep_structured_as_you_need: { + any_data_type: {}, + }, + }, + }, + max_signals: 10, + tags: ['tag 1', 'tag 2', 'any tag you want'], + to: 'now', + from: 'now-6m', + severity: 'high', + language: 'kuery', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + { + framework: 'Some other Framework you want', + tactic: { + id: 'some-other-id', + name: 'Some other name', + reference: 'https://example.com', + }, + technique: [ + { + id: 'some-other-id', + name: 'some other technique name', + reference: 'https://example.com', + }, + ], + }, + ], + references: [ + 'http://www.example.com/some-article-about-attack', + 'Some plain text string here explaining why this is a valid thing to look out for', + ], + timeline_id: 'timeline_id', + timeline_title: 'timeline_title', + updated_by: 'elastic', + version: 1, + query: 'user.name: root or user.name: admin', +});