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 01f5c364ae420..4a4260da2f07e 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 @@ -50,6 +50,7 @@ export const mockPrepackagedRule = (): PrepackagedRules => ({ technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], }, ], + throttle: null, enabled: true, filters: [], immutable: false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index aa9b05eb379a6..13d75cc44992c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -82,6 +82,7 @@ export const getOutputRuleAlertForRest = (): Omit< OutputRuleAlertRest, 'machine_learning_job_id' | 'anomaly_threshold' > => ({ + actions: [], created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index e8b1162b06182..4ffa29c385f28 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -56,6 +56,7 @@ export const createRulesBulkRoute = (router: IRouter) => { .filter(rule => rule.rule_id == null || !dupes.includes(rule.rule_id)) .map(async payloadRule => { const { + actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -77,6 +78,7 @@ export const createRulesBulkRoute = (router: IRouter) => { severity, tags, threat, + throttle, to, type, references, @@ -110,6 +112,7 @@ export const createRulesBulkRoute = (router: IRouter) => { const createdRule = await createRules({ alertsClient, actionsClient, + actions, anomalyThreshold, description, enabled, @@ -133,6 +136,7 @@ export const createRulesBulkRoute = (router: IRouter) => { name, severity, tags, + throttle, to, type, threat, 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 3a440178344da..cee9054cf922e 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 @@ -31,6 +31,7 @@ export const createRulesRoute = (router: IRouter): void => { }, async (context, request, response) => { const { + actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -54,6 +55,7 @@ export const createRulesRoute = (router: IRouter): void => { severity, tags, threat, + throttle, to, type, references, @@ -96,6 +98,7 @@ export const createRulesRoute = (router: IRouter): void => { const createdRule = await createRules({ alertsClient, actionsClient, + actions, anomalyThreshold, description, enabled, @@ -119,6 +122,7 @@ export const createRulesRoute = (router: IRouter): void => { name, severity, tags, + throttle, to, type, threat, 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 920cf97d32a7a..d96f7e4ae168c 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 @@ -111,6 +111,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config return null; } const { + actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -133,6 +134,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config severity, tags, threat, + throttle, to, type, references, @@ -163,6 +165,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config await createRules({ alertsClient, actionsClient, + actions, anomalyThreshold, description, enabled, @@ -189,6 +192,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config to, type, threat, + throttle, references, note, version, @@ -199,6 +203,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config await patchRules({ alertsClient, actionsClient, + actions, savedObjectsClient, description, enabled, @@ -225,6 +230,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config to, type, threat, + throttle, references, note, version, 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 index e64bbe625f5f6..698f58438a5e6 100644 --- 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 @@ -46,6 +46,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { const rules = await Promise.all( request.body.map(async payloadRule => { const { + actions, description, enabled, false_positives: falsePositives, @@ -70,6 +71,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, @@ -79,6 +81,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { const rule = await patchRules({ alertsClient, actionsClient, + actions, description, enabled, falsePositives, @@ -104,6 +107,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, 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 index 2d810d33c6e51..4493bb380d03d 100644 --- 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 @@ -30,6 +30,7 @@ export const patchRulesRoute = (router: IRouter) => { }, async (context, request, response) => { const { + actions, description, enabled, false_positives: falsePositives, @@ -54,6 +55,7 @@ export const patchRulesRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, @@ -76,6 +78,7 @@ export const patchRulesRoute = (router: IRouter) => { const rule = await patchRules({ actionsClient, alertsClient, + actions, description, enabled, falsePositives, @@ -101,6 +104,7 @@ export const patchRulesRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, 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 deb319492258c..6c3c8dffa3dfa 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 @@ -47,6 +47,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rules = await Promise.all( request.body.map(async payloadRule => { const { + actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -73,6 +74,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, @@ -84,6 +86,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, + actions, anomalyThreshold, description, enabled, @@ -112,6 +115,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, 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 c47a412c2e9df..7e56c32ade92a 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 @@ -30,6 +30,7 @@ export const updateRulesRoute = (router: IRouter) => { }, async (context, request, response) => { const { + actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -56,6 +57,7 @@ export const updateRulesRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, @@ -80,6 +82,7 @@ export const updateRulesRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, + actions, anomalyThreshold, description, enabled, @@ -108,6 +111,7 @@ export const updateRulesRoute = (router: IRouter) => { to, type, threat, + throttle, references, note, version, 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 fe7618bca0c75..3d831026256fc 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 @@ -29,6 +29,7 @@ import { OutputError, } from '../utils'; import { hasListsFeature } from '../../feature_flags'; +import { transformAlertToRuleAction } from '../../rules/transform_actions'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -102,6 +103,7 @@ export const transformAlertToRule = ( ruleStatus?: SavedObject ): Partial => { return pickBy((value: unknown) => value != null, { + actions: alert.actions.map(transformAlertToRuleAction), created_at: alert.createdAt.toISOString(), updated_at: alert.updatedAt.toISOString(), created_by: alert.createdBy, @@ -134,6 +136,7 @@ export const transformAlertToRule = ( to: alert.params.to, type: alert.params.type, threat: alert.params.threat, + throttle: alert.throttle, note: alert.params.note, version: alert.params.version, status: ruleStatus?.attributes.status, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 1dce602f3fcac..3727908ac62de 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -19,6 +19,7 @@ import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; export const ruleOutput: RulesSchema = { + actions: [], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 171a34f0d0592..2b18e1b9bf52c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ThreatParams, PrepackagedRules } from '../../types'; +import { AlertAction } from '../../../../../../../../plugins/alerting/common'; +import { ThreatParams, PrepackagedRules, RuleAlertAction } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; @@ -1284,6 +1285,200 @@ describe('add prepackaged rules schema', () => { ); }); + test('The default for "actions" will be an empty array', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + index: ['auditbeat-*'], + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).value.actions + ).toEqual([]); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + expect( + addPrepackagedRulesSchema.validate< + Partial> & { + actions: Array>; + } + >({ + actions: [ + { + id: 'id', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + expect( + addPrepackagedRulesSchema.validate< + Partial> & { + actions: Array>; + } + >({ + actions: [ + { + group: 'group', + action_type_id: 'action_type_id', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + expect( + addPrepackagedRulesSchema.validate< + Partial> & { + actions: Array>; + } + >({ + actions: [ + { + group: 'group', + id: 'id', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + expect( + addPrepackagedRulesSchema.validate< + Partial> & { + actions: Array>; + } + >({ + actions: [ + { + group: 'group', + id: 'id', + action_type_id: 'action_type_id', + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are including "actionTypeId', () => { + expect( + addPrepackagedRulesSchema.validate< + Partial> & { + actions: AlertAction[]; + } + >({ + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('The default for "throttle" will be null', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + index: ['auditbeat-*'], + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).value.throttle + ).toEqual(null); + }); + describe('note', () => { test('You can set note to any string you want', () => { expect( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index 4c60a66141250..da9f9777a01a6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -8,6 +8,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { + actions, enabled, description, false_positives, @@ -31,6 +32,7 @@ import { to, type, threat, + throttle, references, note, version, @@ -53,6 +55,7 @@ import { hasListsFeature } from '../../feature_flags'; * - index is a required field that must exist */ export const addPrepackagedRulesSchema = Joi.object({ + actions: actions.default([]), anomaly_threshold: anomaly_threshold.when('type', { is: 'machine_learning', then: Joi.required(), @@ -101,6 +104,7 @@ export const addPrepackagedRulesSchema = Joi.object({ to: to.default('now'), type: type.required(), threat: threat.default([]), + throttle: throttle.default(null), references: references.default([]), note: note.allow(''), version: version.required(), 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 fa007bba6551a..0bf59759a6db6 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 @@ -217,4 +217,44 @@ describe('create_rules_bulk_schema', () => { '"value" at position 0 fails because [child "note" fails because ["note" must be a string]]' ); }); + + test('The default for "actions" will be an empty array', () => { + expect( + createRulesBulkSchema.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[0].actions + ).toEqual([]); + }); + + test('The default for "throttle" will be null', () => { + expect( + createRulesBulkSchema.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[0].throttle + ).toEqual(null); + }); }); 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 db5097a6f25db..d9c3055512815 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 @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertAction } from '../../../../../../../../plugins/alerting/common'; import { createRulesSchema } from './create_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { ThreatParams, RuleAlertParamsRest, RuleAlertAction } from '../../types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { @@ -1234,6 +1235,184 @@ describe('create rules schema', () => { ); }); + test('The default for "actions" will be an empty array', () => { + expect( + createRulesSchema.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.actions + ).toEqual([]); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + expect( + createRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + expect( + createRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + expect( + createRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + expect( + createRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are including "actionTypeId"', () => { + expect( + createRulesSchema.validate< + Partial< + Omit & { + actions: AlertAction[]; + } + > + >({ + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('The default for "throttle" will be null', () => { + expect( + createRulesSchema.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.throttle + ).toEqual(null); + }); + describe('note', () => { test('You can set note to a string', () => { expect( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index 0aa7317dd8cdc..5213f3faaf486 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -8,6 +8,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { + actions, anomaly_threshold, enabled, description, @@ -32,6 +33,7 @@ import { to, type, threat, + throttle, references, note, version, @@ -44,6 +46,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; import { hasListsFeature } from '../../feature_flags'; export const createRulesSchema = Joi.object({ + actions: actions.default([]), anomaly_threshold: anomaly_threshold.when('type', { is: 'machine_learning', then: Joi.required(), @@ -89,6 +92,7 @@ export const createRulesSchema = Joi.object({ to: to.default('now'), type: type.required(), threat: threat.default([]), + throttle: throttle.default(null), references: references.default([]), note: note.allow(''), version: version.default(1), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index bcb24268fc6c7..ffb49896ef7c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertAction } from '../../../../../../../../plugins/alerting/common'; import { importRulesSchema, importRulesQuerySchema, importRulesPayloadSchema, } from './import_rules_schema'; -import { ThreatParams, ImportRuleAlertRest } from '../../types'; +import { ThreatParams, ImportRuleAlertRest, RuleAlertAction } from '../../types'; import { ImportRulesRequestParams } from '../../rules/types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; @@ -1433,6 +1434,184 @@ describe('import rules schema', () => { ); }); + test('The default for "actions" will be an empty array', () => { + expect( + importRulesSchema.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.actions + ).toEqual([]); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + expect( + importRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + rule_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 "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + expect( + importRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + expect( + importRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + expect( + importRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are including "actionTypeId', () => { + expect( + importRulesSchema.validate< + Partial< + Omit & { + actions: AlertAction[]; + } + > + >({ + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('The default for "throttle" will be null', () => { + expect( + importRulesSchema.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.throttle + ).toEqual(null); + }); + describe('note', () => { test('You can set note to a string', () => { expect( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index 469b59a8e08ad..56aa45659fda7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -9,6 +9,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { id, + actions, created_at, updated_at, created_by, @@ -37,6 +38,7 @@ import { to, type, threat, + throttle, references, note, version, @@ -65,6 +67,7 @@ export const importRulesSchema = Joi.object({ otherwise: Joi.forbidden(), }), id, + actions: actions.default([]), description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -106,6 +109,7 @@ export const importRulesSchema = Joi.object({ to: to.default('now'), type: type.required(), threat: threat.default([]), + throttle: throttle.default(null), references: references.default([]), note: note.allow(''), version: version.default(1), 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 index 6fc1a0c3caa9c..42945e0970cba 100644 --- 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 @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertAction } from '../../../../../../../../plugins/alerting/common'; import { patchRulesSchema } from './patch_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams } from '../../types'; +import { ThreatParams, RuleAlertAction } from '../../types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch rules schema', () => { @@ -1063,6 +1064,148 @@ describe('patch rules schema', () => { }); }); + test('You cannot send in an array of actions that are missing "group"', () => { + expect( + patchRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + expect( + patchRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + expect( + patchRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + expect( + patchRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are including "actionTypeId', () => { + expect( + patchRulesSchema.validate< + Partial< + Omit & { + actions: AlertAction[]; + } + > + >({ + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, 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 index 8bb155d83cf44..52aefa29884c3 100644 --- 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 @@ -8,6 +8,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { + actions, enabled, description, false_positives, @@ -31,6 +32,7 @@ import { to, type, threat, + throttle, references, note, id, @@ -43,6 +45,7 @@ import { hasListsFeature } from '../../feature_flags'; /* eslint-enable @typescript-eslint/camelcase */ export const patchRulesSchema = Joi.object({ + actions, anomaly_threshold, description, enabled, @@ -69,6 +72,7 @@ export const patchRulesSchema = Joi.object({ to, type, threat, + throttle, references, note: note.allow(''), version, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index 75de97a55534b..1574e8f5aa6e1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -12,6 +12,7 @@ import { Either, fold, right, left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { checkTypeDependents } from './check_type_dependents'; import { + actions, anomaly_threshold, description, enabled, @@ -42,6 +43,7 @@ import { timeline_title, type, threat, + throttle, job_status, status_date, last_success_at, @@ -117,6 +119,8 @@ export const dependentRulesSchema = t.partial({ * Instead use dependentRulesSchema and check_type_dependents for how to do those. */ export const partialRulesSchema = t.partial({ + actions, + throttle, status: job_status, status_date, last_success_at, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index d90cb7b1f0829..538c8f754fd6e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -25,6 +25,25 @@ export const file_name = t.string; */ export const filters = t.array(t.unknown); // Filters are not easily type-able yet +/** + * Params is an "object", since it is a type of AlertActionParams which is action templates. + * @see x-pack/plugins/alerting/common/alert.ts + */ +export const action_group = t.string; +export const action_id = t.string; +export const action_action_type_id = t.string; +export const action_params = t.object; +export const action = t.exact( + t.type({ + group: action_group, + id: action_id, + action_type_id: action_action_type_id, + params: action_params, + }) +); + +export const actions = t.array(action); + // TODO: Create a regular expression type or custom date math part type here export const from = t.string; @@ -45,6 +64,7 @@ export const output_index = t.string; export const saved_id = t.string; export const timeline_id = t.string; export const timeline_title = t.string; +export const throttle = t.string; export const anomaly_threshold = PositiveInteger; export const machine_learning_job_id = t.string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 007294293f59b..16e419f389f09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -110,6 +110,18 @@ export const updated_by = Joi.string(); export const version = Joi.number() .integer() .min(1); +export const action_group = Joi.string(); +export const action_id = Joi.string(); +export const action_action_type_id = Joi.string(); +export const action_params = Joi.object(); +export const action = Joi.object({ + group: action_group.required(), + id: action_id.required(), + action_type_id: action_action_type_id.required(), + params: action_params.required(), +}); +export const actions = Joi.array().items(action); +export const throttle = Joi.string().allow(null); export const note = Joi.string(); // NOTE: Experimental list support not being shipped currently and behind a feature flag 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 a0689966a8694..db3709cd6b126 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 @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertAction } from '../../../../../../../../plugins/alerting/common'; import { updateRulesSchema } from './update_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { ThreatParams, RuleAlertParamsRest, RuleAlertAction } from '../../types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { @@ -1253,6 +1254,184 @@ describe('create rules schema', () => { ); }); + test('The default for "actions" will be an empty array', () => { + 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.actions + ).toEqual([]); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + expect( + updateRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + expect( + updateRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + expect( + updateRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', params: {} }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + expect( + updateRulesSchema.validate< + Partial< + Omit & { + actions: Array>; + } + > + >({ + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' + ); + }); + + test('You cannot send in an array of actions that are including "actionTypeId"', () => { + expect( + updateRulesSchema.validate< + Partial< + Omit & { + actions: AlertAction[]; + } + > + >({ + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + 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, + }).error.message + ).toEqual( + 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' + ); + }); + + test('The default for "throttle" will be null', () => { + 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.throttle + ).toEqual(null); + }); + describe('note', () => { test('You can set note to a string', () => { expect( 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 421172cf0b1a1..f842c14f41ae6 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 @@ -8,6 +8,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { + actions, enabled, description, false_positives, @@ -31,6 +32,7 @@ import { to, type, threat, + throttle, references, id, note, @@ -52,6 +54,7 @@ import { hasListsFeature } from '../../feature_flags'; * - id is on here because you can pass in an id to update using it instead of rule_id. */ export const updateRulesSchema = Joi.object({ + actions: actions.default([]), anomaly_threshold: anomaly_threshold.when('type', { is: 'machine_learning', then: Joi.required(), @@ -98,6 +101,7 @@ export const updateRulesSchema = Joi.object({ to: to.default('now'), type: type.required(), threat: threat.default([]), + throttle: throttle.default(null), references: references.default([]), note: note.allow(''), version, 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 0bf9d17d70fdc..db70b90d5a17c 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 @@ -9,10 +9,12 @@ import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; import { hasListsFeature } from '../feature_flags'; +import { transformRuleToAlertAction } from './transform_actions'; export const createRules = ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... + actions, anomalyThreshold, description, enabled, @@ -37,6 +39,7 @@ export const createRules = ({ severity, tags, threat, + throttle, to, type, references, @@ -82,8 +85,8 @@ export const createRules = ({ }, schedule: { interval }, enabled, - actions: [], // TODO: Create and add actions here once we have email, etc... - throttle: null, + actions: actions?.map(transformRuleToAlertAction), + throttle, }, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index 3ed4408138833..695057ccc2f70 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -49,6 +49,7 @@ describe('create_rules_stream_from_ndjson', () => { ]); expect(result).toEqual([ { + actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -69,10 +70,12 @@ describe('create_rules_stream_from_ndjson', () => { max_signals: 100, tags: [], threat: [], + throttle: null, references: [], version: 1, }, { + actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, @@ -93,6 +96,7 @@ describe('create_rules_stream_from_ndjson', () => { max_signals: 100, tags: [], threat: [], + throttle: null, references: [], version: 1, }, @@ -135,6 +139,7 @@ describe('create_rules_stream_from_ndjson', () => { ]); expect(result).toEqual([ { + actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -155,10 +160,12 @@ describe('create_rules_stream_from_ndjson', () => { tags: [], lists: [], threat: [], + throttle: null, references: [], version: 1, }, { + actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, @@ -179,6 +186,7 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }, @@ -204,6 +212,7 @@ describe('create_rules_stream_from_ndjson', () => { ]); expect(result).toEqual([ { + actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -224,10 +233,12 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }, { + actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, @@ -248,6 +259,7 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }, @@ -273,6 +285,7 @@ describe('create_rules_stream_from_ndjson', () => { ]); const resultOrError = result as Error[]; expect(resultOrError[0]).toEqual({ + actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -293,11 +306,13 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }); expect(resultOrError[1].message).toEqual('Unexpected token , in JSON at position 1'); expect(resultOrError[2]).toEqual({ + actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, @@ -318,6 +333,7 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }); @@ -342,6 +358,7 @@ describe('create_rules_stream_from_ndjson', () => { ]); const resultOrError = result as BadRequestError[]; expect(resultOrError[0]).toEqual({ + actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -362,6 +379,7 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }); @@ -369,6 +387,7 @@ describe('create_rules_stream_from_ndjson', () => { 'child "description" fails because ["description" is required]' ); expect(resultOrError[2]).toEqual({ + actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, @@ -389,6 +408,7 @@ describe('create_rules_stream_from_ndjson', () => { lists: [], tags: [], threat: [], + throttle: null, references: [], version: 1, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 532bfbaf469ff..20ddcdc3f5362 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -30,6 +30,7 @@ describe('getExportAll', () => { const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: `${JSON.stringify({ + actions: [], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index f27299436c702..e6d4c68d7108d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -38,6 +38,7 @@ describe('get_export_by_object_ids', () => { const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: `${JSON.stringify({ + actions: [], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', @@ -158,6 +159,7 @@ describe('get_export_by_object_ids', () => { missingRules: [], rules: [ { + actions: [], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index bcbe460fb6a66..801f3d949ed78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -18,6 +18,7 @@ export const installPrepackagedRules = ( ): Array> => rules.reduce>>((acc, rule) => { const { + actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -43,6 +44,7 @@ export const installPrepackagedRules = ( to, type, threat, + throttle, references, note, version, @@ -53,6 +55,7 @@ export const installPrepackagedRules = ( createRules({ alertsClient, actionsClient, + actions, anomalyThreshold, description, enabled, @@ -79,6 +82,7 @@ export const installPrepackagedRules = ( to, type, threat, + throttle, references, note, version, 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 index 4fb73235854c0..be466ffeed27c 100644 --- 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 @@ -11,10 +11,12 @@ import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './ty import { addTags } from './add_tags'; import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion, calculateName, calculateInterval } from './utils'; +import { transformRuleToAlertAction } from './transform_actions'; export const patchRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types + actions, savedObjectsClient, description, falsePositives, @@ -39,12 +41,12 @@ export const patchRules = async ({ severity, tags, threat, + throttle, to, type, references, note, version, - throttle, lists, }: PatchRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); @@ -53,6 +55,7 @@ export const patchRules = async ({ } const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { + actions, description, falsePositives, query, @@ -72,11 +75,11 @@ export const patchRules = async ({ severity, tags, threat, + throttle, to, type, references, version, - throttle, note, lists, }); @@ -116,12 +119,12 @@ export const patchRules = async ({ id: rule.id, data: { tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), - throttle: throttle ?? rule.throttle ?? null, + throttle: throttle !== undefined ? throttle : rule.throttle, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), }, - actions: rule.actions, + actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, params: nextParams, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/transform_actions.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/transform_actions.test.ts new file mode 100644 index 0000000000000..93b5f238be9ed --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/transform_actions.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { transformRuleToAlertAction, transformAlertToRuleAction } from './transform_actions'; + +describe('transform_actions', () => { + test('it should transform RuleAlertAction[] to AlertAction[]', () => { + const ruleAction = { + id: 'id', + group: 'group', + action_type_id: 'action_type_id', + params: {}, + }; + const alertAction = transformRuleToAlertAction(ruleAction); + expect(alertAction).toEqual({ + id: ruleAction.id, + group: ruleAction.group, + actionTypeId: ruleAction.action_type_id, + params: ruleAction.params, + }); + }); + + test('it should transform AlertAction[] to RuleAlertAction[]', () => { + const alertAction = { + id: 'id', + group: 'group', + actionTypeId: 'actionTypeId', + params: {}, + }; + const ruleAction = transformAlertToRuleAction(alertAction); + expect(ruleAction).toEqual({ + id: alertAction.id, + group: alertAction.group, + action_type_id: alertAction.actionTypeId, + params: alertAction.params, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/transform_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/transform_actions.ts new file mode 100644 index 0000000000000..c1c17d2c70836 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/transform_actions.ts @@ -0,0 +1,32 @@ +/* + * 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 { AlertAction } from '../../../../../../../plugins/alerting/common'; +import { RuleAlertAction } from '../types'; + +export const transformRuleToAlertAction = ({ + group, + id, + action_type_id, + params, +}: RuleAlertAction): AlertAction => ({ + group, + id, + params, + actionTypeId: action_type_id, +}); + +export const transformAlertToRuleAction = ({ + group, + id, + actionTypeId, + params, +}: AlertAction): RuleAlertAction => ({ + group, + id, + params, + action_type_id: actionTypeId, +}); 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 1051ac28885b8..cc67622176a04 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 @@ -19,6 +19,7 @@ export const updatePrepackagedRules = async ( ): Promise => { await rules.forEach(async rule => { const { + actions, description, false_positives: falsePositives, from, @@ -39,9 +40,9 @@ export const updatePrepackagedRules = async ( to, type, threat, + throttle, references, version, - throttle, note, } = rule; @@ -50,6 +51,7 @@ export const updatePrepackagedRules = async ( return patchRules({ alertsClient, actionsClient, + actions, description, falsePositives, from, @@ -73,9 +75,9 @@ export const updatePrepackagedRules = async ( to, type, threat, + throttle, references, version, - throttle, note, }); }); 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 b2a1d2a6307d2..ead065d7f198d 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 @@ -11,10 +11,12 @@ import { addTags } from './add_tags'; import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion } from './utils'; import { hasListsFeature } from '../feature_flags'; +import { transformRuleToAlertAction } from './transform_actions'; export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types + actions, savedObjectsClient, description, falsePositives, @@ -39,11 +41,11 @@ export const updateRules = async ({ severity, tags, threat, + throttle, to, type, references, version, - throttle, note, lists, }: UpdateRuleParams): Promise => { @@ -53,6 +55,7 @@ export const updateRules = async ({ } const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { + actions, description, falsePositives, query, @@ -72,11 +75,11 @@ export const updateRules = async ({ severity, tags, threat, + throttle, to, type, references, version, - throttle, note, }); @@ -89,8 +92,8 @@ export const updateRules = async ({ tags: addTags(tags, rule.params.ruleId, immutable), name, schedule: { interval }, - actions: rule.actions, - throttle: throttle ?? rule.throttle ?? null, + actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, + throttle: throttle !== undefined ? throttle : rule.throttle, params: { description, ruleId: rule.params.ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index c30635c9d1490..c86696d6ec5eb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -25,6 +25,7 @@ describe('buildBulkBody', () => { ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -32,6 +33,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -60,6 +62,7 @@ describe('buildBulkBody', () => { original_time: 'someTimeStamp', status: 'open', rule: { + actions: [], id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], @@ -132,6 +135,7 @@ describe('buildBulkBody', () => { ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -139,6 +143,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -176,6 +181,7 @@ describe('buildBulkBody', () => { original_time: 'someTimeStamp', status: 'open', rule: { + actions: [], id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], @@ -247,6 +253,7 @@ describe('buildBulkBody', () => { ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -254,6 +261,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -290,6 +298,7 @@ describe('buildBulkBody', () => { original_time: 'someTimeStamp', status: 'open', rule: { + actions: [], id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], @@ -359,6 +368,7 @@ describe('buildBulkBody', () => { ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -366,6 +376,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -397,6 +408,7 @@ describe('buildBulkBody', () => { original_time: 'someTimeStamp', status: 'open', rule: { + actions: [], id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts index e77755073b374..adbd5f81d372a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts @@ -8,12 +8,13 @@ import { SignalSourceHit, SignalHit } from './types'; import { buildRule } from './build_rule'; import { buildSignal } from './build_signal'; import { buildEventTypeSignal } from './build_event_type_signal'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RuleAlertAction } from '../types'; interface BuildBulkBodyParams { doc: SignalSourceHit; ruleParams: RuleTypeParams; id: string; + actions: RuleAlertAction[]; name: string; createdAt: string; createdBy: string; @@ -22,6 +23,7 @@ interface BuildBulkBodyParams { interval: string; enabled: boolean; tags: string[]; + throttle: string | null; } // format search_after result for signals index. @@ -30,6 +32,7 @@ export const buildBulkBody = ({ ruleParams, id, name, + actions, createdAt, createdBy, updatedAt, @@ -37,8 +40,10 @@ export const buildBulkBody = ({ interval, enabled, tags, + throttle, }: BuildBulkBodyParams): SignalHit => { const rule = buildRule({ + actions, ruleParams, id, name, @@ -49,6 +54,7 @@ export const buildBulkBody = ({ updatedBy, interval, tags, + throttle, }); const signal = buildSignal(doc, rule); const event = buildEventTypeSignal(doc); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index 499e3e9c88a85..37d7ed8a51082 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -27,6 +27,7 @@ describe('buildRule', () => { }, ]; const rule = buildRule({ + actions: [], ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -37,8 +38,10 @@ describe('buildRule', () => { updatedBy: 'elastic', interval: 'some interval', tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); const expected: Partial = { + actions: [], created_by: 'elastic', description: 'Detecting root and admin users', enabled: false, @@ -106,10 +109,11 @@ describe('buildRule', () => { expect(rule).toEqual(expected); }); - test('it omits a null value such as if enabled is null if is present', () => { + test('it omits a null value such as if "enabled" is null if is present', () => { const ruleParams = sampleRuleAlertParams(); ruleParams.filters = undefined; const rule = buildRule({ + actions: [], ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -120,8 +124,10 @@ describe('buildRule', () => { updatedBy: 'elastic', interval: 'some interval', tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); const expected: Partial = { + actions: [], created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -178,10 +184,11 @@ describe('buildRule', () => { expect(rule).toEqual(expected); }); - test('it omits a null value such as if filters is undefined if is present', () => { + test('it omits a null value such as if "filters" is undefined if is present', () => { const ruleParams = sampleRuleAlertParams(); ruleParams.filters = undefined; const rule = buildRule({ + actions: [], ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -192,8 +199,84 @@ describe('buildRule', () => { updatedBy: 'elastic', interval: 'some interval', tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); const expected: Partial = { + actions: [], + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + note: '', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + updated_by: 'elastic', + version: 1, + updated_at: rule.updated_at, + created_at: rule.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if "throttle" is undefined if is present', () => { + const ruleParams = sampleRuleAlertParams(); + const rule = buildRule({ + actions: [], + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, + }); + const expected: Partial = { + actions: [], created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index a1bee162c9280..e94ca18b186e4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -5,12 +5,13 @@ */ import { pickBy } from 'lodash/fp'; -import { RuleTypeParams, OutputRuleAlertRest } from '../types'; +import { RuleTypeParams, OutputRuleAlertRest, RuleAlertAction } from '../types'; interface BuildRuleParams { ruleParams: RuleTypeParams; name: string; id: string; + actions: RuleAlertAction[]; enabled: boolean; createdAt: string; createdBy: string; @@ -18,12 +19,14 @@ interface BuildRuleParams { updatedBy: string; interval: string; tags: string[]; + throttle: string | null; } export const buildRule = ({ ruleParams, name, id, + actions, enabled, createdAt, createdBy, @@ -31,10 +34,12 @@ export const buildRule = ({ updatedBy, interval, tags, + throttle, }: BuildRuleParams): Partial => { return pickBy((value: unknown) => value != null, { id, rule_id: ruleParams.ruleId, + actions, false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, @@ -62,6 +67,7 @@ export const buildRule = ({ created_by: createdBy, updated_by: updatedBy, threat: ruleParams.threat, + throttle, version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 1ab34f26d4b70..95adb90172404 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -9,11 +9,12 @@ import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../../src/core/server'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RuleAlertAction } from '../types'; import { singleBulkCreate } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; interface BulkCreateMlSignalsParams { + actions: RuleAlertAction[]; someResult: AnomalyResults; ruleParams: RuleTypeParams; services: AlertServices; @@ -28,6 +29,7 @@ interface BulkCreateMlSignalsParams { interval: string; enabled: boolean; tags: string[]; + throttle: string | null; } interface EcsAnomaly extends Anomaly { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 09daae8485381..315a5dd88d94e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -43,6 +43,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -52,6 +53,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(result).toEqual(true); @@ -99,6 +101,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -108,6 +111,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(result).toEqual(true); @@ -126,6 +130,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -135,6 +140,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); @@ -160,6 +166,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -169,6 +176,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); @@ -194,6 +202,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -203,6 +212,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(result).toEqual(true); }); @@ -230,6 +240,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -239,6 +250,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(result).toEqual(true); }); @@ -266,6 +278,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -275,6 +288,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(result).toEqual(true); }); @@ -304,6 +318,7 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -313,6 +328,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(result).toEqual(false); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index f54ad67af4a48..a12778d5b8f16 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -5,7 +5,7 @@ */ import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RuleAlertAction } from '../types'; import { Logger } from '../../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; @@ -20,6 +20,7 @@ interface SearchAfterAndBulkCreateParams { inputIndexPattern: string[]; signalsIndex: string; name: string; + actions: RuleAlertAction[]; createdAt: string; createdBy: string; updatedBy: string; @@ -29,6 +30,7 @@ interface SearchAfterAndBulkCreateParams { pageSize: number; filter: unknown; tags: string[]; + throttle: string | null; } // search_after through documents and re-index using bulk endpoint. @@ -41,6 +43,7 @@ export const searchAfterAndBulkCreate = async ({ inputIndexPattern, signalsIndex, filter, + actions, name, createdAt, createdBy, @@ -50,6 +53,7 @@ export const searchAfterAndBulkCreate = async ({ enabled, pageSize, tags, + throttle, }: SearchAfterAndBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; @@ -63,6 +67,7 @@ export const searchAfterAndBulkCreate = async ({ logger, id, signalsIndex, + actions, name, createdAt, createdBy, @@ -71,6 +76,7 @@ export const searchAfterAndBulkCreate = async ({ interval, enabled, tags, + throttle, }); const totalHits = typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; @@ -127,6 +133,7 @@ export const searchAfterAndBulkCreate = async ({ logger, id, signalsIndex, + actions, name, createdAt, createdBy, @@ -135,6 +142,7 @@ export const searchAfterAndBulkCreate = async ({ interval, enabled, tags, + throttle, }); logger.debug('finished next bulk index'); } catch (exc) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 7a4dcf68e0ca9..89dcd3274ebed 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -67,6 +67,7 @@ export const signalRulesAlertType = ({ }); const { + actions, name, tags, createdAt, @@ -74,6 +75,7 @@ export const signalRulesAlertType = ({ updatedBy, enabled, schedule: { interval }, + throttle, } = savedObject.attributes; const updatedAt = savedObject.updated_at ?? ''; @@ -118,6 +120,8 @@ export const signalRulesAlertType = ({ } creationSucceeded = await bulkCreateMlSignals({ + actions, + throttle, someResult: anomalyResults, ruleParams: params, services, @@ -180,6 +184,7 @@ export const signalRulesAlertType = ({ inputIndexPattern: inputIndex, signalsIndex: outputIndex, filter: esFilter, + actions, name, createdBy, createdAt, @@ -189,6 +194,7 @@ export const signalRulesAlertType = ({ enabled, pageSize: searchAfterSize, tags, + throttle, }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 09e2c6b4fd586..afabd4c44de7d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -151,6 +151,7 @@ describe('singleBulkCreate', () => { logger: mockLogger, id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, + actions: [], name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', @@ -159,6 +160,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(successfulsingleBulkCreate).toEqual(true); }); @@ -182,6 +184,7 @@ describe('singleBulkCreate', () => { id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -189,6 +192,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(successfulsingleBulkCreate).toEqual(true); }); @@ -204,6 +208,7 @@ describe('singleBulkCreate', () => { id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -211,6 +216,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(successfulsingleBulkCreate).toEqual(true); }); @@ -227,6 +233,7 @@ describe('singleBulkCreate', () => { id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -234,6 +241,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(mockLogger.error).not.toHaveBeenCalled(); @@ -252,6 +260,7 @@ describe('singleBulkCreate', () => { id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', + actions: [], createdAt: '2020-01-28T15:58:34.810Z', updatedAt: '2020-01-28T15:59:14.004Z', createdBy: 'elastic', @@ -259,6 +268,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], + throttle: null, }); expect(mockLogger.error).toHaveBeenCalled(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index 7d6d6d99fa422..333a938e09d45 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -8,7 +8,7 @@ import { countBy, isEmpty } from 'lodash'; import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { SignalSearchResponse, BulkResponse } from './types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RuleAlertAction } from '../types'; import { generateId } from './utils'; import { buildBulkBody } from './build_bulk_body'; import { Logger } from '../../../../../../../../src/core/server'; @@ -20,6 +20,7 @@ interface SingleBulkCreateParams { logger: Logger; id: string; signalsIndex: string; + actions: RuleAlertAction[]; name: string; createdAt: string; createdBy: string; @@ -28,6 +29,7 @@ interface SingleBulkCreateParams { interval: string; enabled: boolean; tags: string[]; + throttle: string | null; } /** @@ -60,6 +62,7 @@ export const singleBulkCreate = async ({ logger, id, signalsIndex, + actions, name, createdAt, createdBy, @@ -68,6 +71,7 @@ export const singleBulkCreate = async ({ interval, enabled, tags, + throttle, }: SingleBulkCreateParams): Promise => { someResult.hits.hits = filterDuplicateRules(id, someResult); @@ -99,6 +103,7 @@ export const singleBulkCreate = async ({ doc, ruleParams, id, + actions, name, createdAt, createdBy, @@ -107,6 +112,7 @@ export const singleBulkCreate = async ({ interval, enabled, tags, + throttle, }), ]); const start = performance.now(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 1ee3d4f0eb8e4..06acff825f68e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParams, OutputRuleAlertRest } from '../types'; +import { RuleAlertParams, OutputRuleAlertRest, RuleAlertAction } from '../types'; import { SearchResponse } from '../../types'; import { AlertType, @@ -147,6 +147,7 @@ export interface SignalHit { } export interface AlertAttributes { + actions: RuleAlertAction[]; enabled: boolean; name: string; tags: string[]; @@ -156,4 +157,5 @@ export interface AlertAttributes { schedule: { interval: string; }; + throttle: string | null; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 5973a1dbe5f18..2cbdc7db3ba64 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertAction } from '../../../../../../plugins/alerting/common'; import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; @@ -23,6 +24,10 @@ export interface ThreatParams { technique: IMitreAttack[]; } +export type RuleAlertAction = Omit & { + action_type_id: string; +}; + // Notice below we are using lists: ListsDefaultArraySchema[]; which is coming directly from the response output section. // TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types // We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove @@ -30,6 +35,7 @@ export interface ThreatParams { export type RuleType = 'query' | 'saved_query' | 'machine_learning'; export interface RuleAlertParams { + actions: RuleAlertAction[]; anomalyThreshold: number | undefined; description: string; note: string | undefined | null; @@ -59,11 +65,14 @@ export interface RuleAlertParams { threat: ThreatParams[] | undefined | null; type: RuleType; version: number; - throttle?: string; + throttle: string | null; lists: ListsDefaultArraySchema | null | undefined; } -export type RuleTypeParams = Omit; +export type RuleTypeParams = Omit< + RuleAlertParams, + 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' +>; export type RuleAlertParamsRest = Omit< RuleAlertParams, 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 index 6e2a391ec14e1..c92e351ed5918 100644 --- 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 @@ -128,6 +128,7 @@ export const binaryToString = (res: any, callback: any): void => { * This is the typical output of a simple rule that Kibana will output with all the defaults. */ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => ({ + actions: [], created_by: 'elastic', description: 'Simple Rule Query', enabled: true, @@ -244,6 +245,7 @@ export const ruleToNdjson = (rule: Partial): Buffer => { * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ + actions: [], name: 'Complex Rule Query', description: 'Complex Rule Query', false_positives: [ @@ -327,6 +329,7 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ + actions: [], created_by: 'elastic', name: 'Complex Rule Query', description: 'Complex Rule Query',