diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 74c127365ddee4..542cbe89160329 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -275,7 +275,12 @@ export type To = t.TypeOf; export const toOrUndefined = t.union([to, t.undefined]); export type ToOrUndefined = t.TypeOf; -export const type = t.keyof({ machine_learning: null, query: null, saved_query: null }); +export const type = t.keyof({ + machine_learning: null, + query: null, + saved_query: null, + threshold: null, +}); export type Type = t.TypeOf; export const typeOrUndefined = t.union([type, t.undefined]); @@ -369,6 +374,17 @@ export type Threat = t.TypeOf; export const threatOrUndefined = t.union([threat, t.undefined]); export type ThreatOrUndefined = t.TypeOf; +export const threshold = t.exact( + t.type({ + field: t.string, + value: PositiveIntegerGreaterThanZero, + }) +); +export type Threshold = t.TypeOf; + +export const thresholdOrUndefined = t.union([threshold, t.undefined]); +export type ThresholdOrUndefined = t.TypeOf; + export const created_at = IsoDateString; export const updated_at = IsoDateString; export const updated_by = t.string; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index bf96be5e688fa0..aebc3361f6e49b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -25,6 +25,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, References, @@ -111,6 +112,7 @@ export const addPrepackagedRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts index 793d4b04ed0e52..f844d0e86e1f92 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts @@ -8,7 +8,7 @@ import { AddPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; import { addPrepackagedRuleValidateTypeDependents } from './add_prepackaged_rules_type_dependents'; import { getAddPrepackagedRulesSchemaMock } from './add_prepackaged_rules_schema.mock'; -describe('create_rules_type_dependents', () => { +describe('add_prepackaged_rules_type_dependents', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { const schema: AddPrepackagedRulesSchema = { ...getAddPrepackagedRulesSchemaMock(), @@ -68,4 +68,26 @@ describe('create_rules_type_dependents', () => { const errors = addPrepackagedRuleValidateTypeDependents(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + type: 'threshold', + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts index 2788c331154d21..6a51f724fc9e6d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts @@ -92,6 +92,19 @@ export const validateTimelineTitle = (rule: AddPrepackagedRulesSchema): string[] return []; }; +export const validateThreshold = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const addPrepackagedRuleValidateTypeDependents = ( schema: AddPrepackagedRulesSchema ): string[] => { @@ -103,5 +116,6 @@ export const addPrepackagedRuleValidateTypeDependents = ( ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts index 0debe01e5a4d74..308b3c24010fbd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -28,6 +28,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, Version, @@ -106,6 +107,7 @@ export const createRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index ebf0b2e591ca9f..43f0901912271c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -65,4 +65,26 @@ describe('create_rules_type_dependents', () => { const errors = createRuleValidateTypeDependents(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + type: 'threshold', + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts index aad2a2c4a92064..af665ff8c81d2d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts @@ -92,6 +92,19 @@ export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => { return []; }; +export const validateThreshold = (rule: CreateRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => { return [ ...validateAnomalyThreshold(schema), @@ -101,5 +114,6 @@ export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): str ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index f61a1546e3e8a3..d141ca56828b6a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -27,6 +27,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, Version, @@ -125,6 +126,7 @@ export const importRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts index f9b989c81e5337..4b047ee6b71987 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts @@ -65,4 +65,26 @@ describe('import_rules_type_dependents', () => { const errors = importRuleValidateTypeDependents(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + type: 'threshold', + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts index 59191a4fe3121d..269181449e9e94 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts @@ -92,6 +92,19 @@ export const validateTimelineTitle = (rule: ImportRulesSchema): string[] => { return []; }; +export const validateThreshold = (rule: ImportRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const importRuleValidateTypeDependents = (schema: ImportRulesSchema): string[] => { return [ ...validateAnomalyThreshold(schema), @@ -101,5 +114,6 @@ export const importRuleValidateTypeDependents = (schema: ImportRulesSchema): str ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 070f3ccfd03b06..dd325c1a5034fd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -33,6 +33,7 @@ import { enabled, tags, threat, + threshold, throttle, references, to, @@ -89,6 +90,7 @@ export const patchRulesSchema = t.exact( tags, to, threat, + threshold, throttle, timestamp_override, references, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.test.ts similarity index 79% rename from x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.test.ts index a388e69332072a..bafaf6f9e22035 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.test.ts @@ -78,4 +78,26 @@ describe('patch_rules_type_dependents', () => { const errors = patchRuleValidateTypeDependents(schema); expect(errors).toEqual(['either "id" or "rule_id" must be set']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + type: 'threshold', + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts index 554cdb822762f8..a229771a7c05cd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts @@ -66,6 +66,19 @@ export const validateId = (rule: PatchRulesSchema): string[] => { } }; +export const validateThreshold = (rule: PatchRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const patchRuleValidateTypeDependents = (schema: PatchRulesSchema): string[] => { return [ ...validateId(schema), @@ -73,5 +86,6 @@ export const patchRuleValidateTypeDependents = (schema: PatchRulesSchema): strin ...validateLanguage(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts index 98082c2de838a3..4f284eedef3fda 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts @@ -28,6 +28,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, version, @@ -114,6 +115,7 @@ export const updateRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts index a63c8243cb5f15..91b11ea758e93f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts @@ -85,4 +85,26 @@ describe('update_rules_type_dependents', () => { const errors = updateRuleValidateTypeDependents(schema); expect(errors).toEqual(['either "id" or "rule_id" must be set']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + type: 'threshold', + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index 9204f727b2660a..44182d250c8013 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -102,6 +102,19 @@ export const validateId = (rule: UpdateRulesSchema): string[] => { } }; +export const validateThreshold = (rule: UpdateRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => { return [ ...validateId(schema), @@ -112,5 +125,6 @@ export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): str ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index c0fec2b2eefc2d..4bd18a13e4ebb0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -44,6 +44,7 @@ import { timeline_title, type, threat, + threshold, throttle, job_status, status_date, @@ -123,6 +124,9 @@ export const dependentRulesSchema = t.partial({ // ML fields anomaly_threshold, machine_learning_job_id, + + // Threshold fields + threshold, }); /** @@ -202,7 +206,7 @@ export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mi }; export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { + if (['query', 'saved_query', 'threshold'].includes(typeAndTimelineOnly.type)) { return [ t.exact(t.type({ query: dependentRulesSchema.props.query })), t.exact(t.type({ language: dependentRulesSchema.props.language })), @@ -225,6 +229,17 @@ export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] } }; +export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'threshold') { + return [ + t.exact(t.type({ threshold: dependentRulesSchema.props.threshold })), + t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), + ]; + } else { + return []; + } +}; + export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { const dependents: t.Mixed[] = [ t.exact(requiredRulesSchema), @@ -233,6 +248,7 @@ export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed ...addTimelineTitle(typeAndTimelineOnly), ...addQueryFields(typeAndTimelineOnly), ...addMlFields(typeAndTimelineOnly), + ...addThresholdFields(typeAndTimelineOnly), ]; if (dependents.length > 1) { diff --git a/x-pack/plugins/security_solution/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts index 431d716a9f205c..7c752bca49dbdf 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/types.ts @@ -15,5 +15,6 @@ export const RuleTypeSchema = t.keyof({ query: null, saved_query: null, machine_learning: null, + threshold: null, }); export type RuleType = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 1213312e2a22c7..24bfeaa4dae1a6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -53,6 +53,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); @@ -65,6 +66,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); const expected = { @@ -250,6 +252,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); // @ts-ignore @@ -279,6 +282,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); // @ts-ignore @@ -297,6 +301,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); @@ -326,6 +331,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: ecsDataMock, + nonEcsData: [], updateTimelineIsLoading, }); @@ -350,6 +356,7 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, + nonEcsData: [], updateTimelineIsLoading, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 24f292cf9135bc..11c13c2358e940 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable complexity */ + import dateMath from '@elastic/datemath'; -import { getOr, isEmpty } from 'lodash/fp'; +import { get, getOr, isEmpty, find } from 'lodash/fp'; import moment from 'moment'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; @@ -30,6 +32,8 @@ import { replaceTemplateFieldFromMatchFilters, replaceTemplateFieldFromDataProviders, } from './helpers'; +import { KueryFilterQueryKind } from '../../../common/store'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { @@ -99,10 +103,45 @@ export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { return { to, from }; }; +export const getThresholdAggregationDataProvider = ( + ecsData: Ecs, + nonEcsData: TimelineNonEcsData[] +): DataProvider[] => { + const aggregationField = ecsData.signal?.rule?.threshold.field; + const aggregationValue = + get(aggregationField, ecsData) ?? find(['field', aggregationField], nonEcsData)?.value; + const dataProviderValue = Array.isArray(aggregationValue) + ? aggregationValue[0] + : aggregationValue; + + if (!dataProviderValue) { + return []; + } + + const aggregationFieldId = aggregationField.replace('.', '-'); + + return [ + { + and: [], + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-${aggregationFieldId}-${dataProviderValue}`, + name: ecsData.signal?.rule?.threshold.field, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: aggregationField, + value: dataProviderValue, + operator: ':', + }, + }, + ]; +}; + export const sendAlertToTimelineAction = async ({ apolloClient, createTimeline, ecsData, + nonEcsData, updateTimelineIsLoading, }: SendAlertToTimelineActionProps) => { let openAlertInBasicTimeline = true; @@ -146,7 +185,7 @@ export const sendAlertToTimelineAction = async ({ timeline.timelineType ); - createTimeline({ + return createTimeline({ from, timeline: { ...timeline, @@ -186,8 +225,62 @@ export const sendAlertToTimelineAction = async ({ } } - if (openAlertInBasicTimeline) { - createTimeline({ + if ( + ecsData.signal?.rule?.type?.length && + ecsData.signal?.rule?.type[0] === 'threshold' && + openAlertInBasicTimeline + ) { + return createTimeline({ + from, + timeline: { + ...timelineDefaults, + dataProviders: [ + { + and: [], + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + name: ecsData._id, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '_id', + value: ecsData._id, + operator: ':', + }, + }, + ...getThresholdAggregationDataProvider(ecsData, nonEcsData), + ], + id: 'timeline-1', + dateRange: { + start: from, + end: to, + }, + eventType: 'all', + kqlQuery: { + filterQuery: { + kuery: { + kind: ecsData.signal?.rule?.language?.length + ? (ecsData.signal?.rule?.language[0] as KueryFilterQueryKind) + : 'kuery', + expression: ecsData.signal?.rule?.query?.length ? ecsData.signal?.rule?.query[0] : '', + }, + serializedQuery: ecsData.signal?.rule?.query?.length + ? ecsData.signal?.rule?.query[0] + : '', + }, + filterQueryDraft: { + kind: ecsData.signal?.rule?.language?.length + ? (ecsData.signal?.rule?.language[0] as KueryFilterQueryKind) + : 'kuery', + expression: ecsData.signal?.rule?.query?.length ? ecsData.signal?.rule?.query[0] : '', + }, + }, + }, + to, + ruleNote: noteContent, + }); + } else { + return createTimeline({ from, timeline: { ...timelineDefaults, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 319575c9c307f5..6f1f2e46dce3d9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -309,11 +309,12 @@ export const getAlertActions = ({ displayType: 'icon', iconType: 'timeline', id: 'sendAlertToTimeline', - onClick: ({ ecsData }: TimelineRowActionOnClick) => + onClick: ({ ecsData, data }: TimelineRowActionOnClick) => sendAlertToTimelineAction({ apolloClient, createTimeline, ecsData, + nonEcsData: data, updateTimelineIsLoading, }), width: DEFAULT_ICON_BUTTON_WIDTH, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index b127ff04eca46d..34d18b4dedba6e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -7,7 +7,7 @@ import ApolloClient from 'apollo-client'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; -import { Ecs } from '../../../graphql/types'; +import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { inputsModel } from '../../../common/store'; @@ -53,6 +53,7 @@ export interface SendAlertToTimelineActionProps { apolloClient?: ApolloClient<{}>; createTimeline: CreateTimeline; ecsData: Ecs; + nonEcsData: TimelineNonEcsData[]; updateTimelineIsLoading: UpdateTimelineLoading; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index b82d1c0a36ab28..41ee91845a8ec8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -403,5 +403,17 @@ describe('helpers', () => { expect(result.description).toEqual('Query'); }); + + it('returns the label for a threshold type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threshold'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a threshold type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threshold'); + + expect(result.description).toEqual('Threshold'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index a0d43c3abf5c17..8393f2230dcfef 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -19,6 +19,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; import { RuleType } from '../../../../../common/detection_engine/types'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -132,10 +133,10 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription {tactic != null ? tactic.text : ''} - {singleThreat.technique.map((technique) => { + {singleThreat.technique.map((technique, listIndex) => { const myTechnique = techniquesOptions.find((t) => t.id === technique.id); return ( - + [ + { + title: label, + description: ( + <> + {isEmpty(threshold.field[0]) + ? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}` + : `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${threshold.field[0]} >= ${threshold.value}`} + + ), + }, +]; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 0a7e666d65aef1..5a2a44a284e3b4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { StepRuleDescriptionComponent, @@ -367,6 +367,52 @@ describe('description_step', () => { }); }); + describe('threshold', () => { + test('returns threshold description when threshold exist and field is empty', () => { + const mockThreshold = { + isNew: false, + threshold: { + field: [''], + value: 100, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'threshold', + 'Threshold label', + mockThreshold, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threshold label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + expect(mount(result[0].description as React.ReactElement).html()).toContain( + 'All results >= 100' + ); + }); + + test('returns threshold description when threshold exist and field is set', () => { + const mockThreshold = { + isNew: false, + threshold: { + field: ['user.name'], + value: 100, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'threshold', + 'Threshold label', + mockThreshold, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threshold label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + expect(mount(result[0].description as React.ReactElement).html()).toContain( + 'Results aggregated by user.name >= 100' + ); + }); + }); + describe('references', () => { test('returns array of ListItems when references exist', () => { const result: ListItems[] = getDescriptionItem( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 8f3a76c6aea577..51624d04cb58b1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -35,6 +35,7 @@ import { buildUrlsDescription, buildNoteDescription, buildRuleTypeDescription, + buildThresholdDescription, } from './helpers'; import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; import { buildMlJobDescription } from './ml_job_description'; @@ -179,6 +180,9 @@ export const getDescriptionItem = ( (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' ); return buildThreatDescription({ label, threat }); + } else if (field === 'threshold') { + const threshold = get(field, data); + return buildThresholdDescription(label, threshold); } else if (field === 'references') { const urls: string[] = get(field, data); return buildUrlsDescription(label, urls); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx index 3e639ede7a18b4..76217964a87cb4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx @@ -41,6 +41,13 @@ export const QUERY_TYPE_DESCRIPTION = i18n.translate( } ); +export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription', + { + defaultMessage: 'Threshold', + } +); + export const ML_JOB_STARTED = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription', { @@ -54,3 +61,17 @@ export const ML_JOB_STOPPED = i18n.translate( defaultMessage: 'Stopped', } ); + +export const THRESHOLD_RESULTS_ALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription', + { + defaultMessage: 'All results', + } +); + +export const THRESHOLD_RESULTS_AGGREGATED_BY = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription', + { + defaultMessage: 'Results aggregated by', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 3dad53f532a5b5..6546c1ba59d84f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -4,52 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCard, - EuiFlexGrid, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiText, -} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; +import { MlCardDescription } from './ml_card_description'; -const MlCardDescription = ({ - subscriptionUrl, - hasValidLicense = false, -}: { - subscriptionUrl: string; - hasValidLicense?: boolean; -}) => ( - - {hasValidLicense ? ( - i18n.ML_TYPE_DESCRIPTION - ) : ( - - - - ), - }} - /> - )} - -); +const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; interface SelectRuleTypeProps { describedByIds?: string[]; @@ -75,11 +40,39 @@ export const SelectRuleType: React.FC = ({ ); const setMl = useCallback(() => setType('machine_learning'), [setType]); const setQuery = useCallback(() => setType('query'), [setType]); + const setThreshold = useCallback(() => setType('threshold'), [setType]); const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { path: '#/management/stack/license_management', }); + const querySelectableConfig = useMemo( + () => ({ + isDisabled: isReadOnly, + onClick: setQuery, + isSelected: !isMlRule(ruleType) && !isThresholdRule(ruleType), + }), + [isReadOnly, ruleType, setQuery] + ); + + const mlSelectableConfig = useMemo( + () => ({ + isDisabled: mlCardDisabled, + onClick: setMl, + isSelected: isMlRule(ruleType), + }), + [mlCardDisabled, ruleType, setMl] + ); + + const thresholdSelectableConfig = useMemo( + () => ({ + isDisabled: isReadOnly, + onClick: setThreshold, + isSelected: isThresholdRule(ruleType), + }), + [isReadOnly, ruleType, setThreshold] + ); + return ( = ({ title={i18n.QUERY_TYPE_TITLE} description={i18n.QUERY_TYPE_DESCRIPTION} icon={} - selectable={{ - isDisabled: isReadOnly, - onClick: setQuery, - isSelected: !isMlRule(ruleType), - }} + isDisabled={querySelectableConfig.isDisabled && !querySelectableConfig.isSelected} + selectable={querySelectableConfig} /> @@ -109,12 +99,20 @@ export const SelectRuleType: React.FC = ({ } icon={} - isDisabled={mlCardDisabled} - selectable={{ - isDisabled: mlCardDisabled, - onClick: setMl, - isSelected: isMlRule(ruleType), - }} + isDisabled={mlSelectableConfig.isDisabled && !mlSelectableConfig.isSelected} + selectable={mlSelectableConfig} + /> + + + } + isDisabled={ + thresholdSelectableConfig.isDisabled && !thresholdSelectableConfig.isSelected + } + selectable={thresholdSelectableConfig} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx new file mode 100644 index 00000000000000..2171c93e47d63f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiLink } from '@elastic/eui'; +import React from 'react'; + +import { ML_TYPE_DESCRIPTION } from './translations'; + +interface MlCardDescriptionProps { + subscriptionUrl: string; + hasValidLicense?: boolean; +} + +const MlCardDescriptionComponent: React.FC = ({ + subscriptionUrl, + hasValidLicense = false, +}) => ( + + {hasValidLicense ? ( + ML_TYPE_DESCRIPTION + ) : ( + + + + ), + }} + /> + )} + +); + +MlCardDescriptionComponent.displayName = 'MlCardDescriptionComponent'; + +export const MlCardDescription = React.memo(MlCardDescriptionComponent); + +MlCardDescription.displayName = 'MlCardDescription'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts index 8b92d20616f7cf..3b85a7dfc765c7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts @@ -33,3 +33,17 @@ export const ML_TYPE_DESCRIPTION = i18n.translate( defaultMessage: 'Select ML job to detect anomalous activity.', } ); + +export const THRESHOLD_TYPE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle', + { + defaultMessage: 'Threshold', + } +); + +export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription', + { + defaultMessage: 'Aggregate query results to detect when number of matches exceeds threshold.', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 864f953bff1e1e..c7d70684b34cfd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -35,12 +35,14 @@ import { MlJobSelect } from '../ml_job_select'; import { PickTimeline } from '../pick_timeline'; import { StepContentWrapper } from '../step_content_wrapper'; import { NextStep } from '../next_step'; +import { ThresholdInput } from '../threshold_input'; import { Field, Form, - FormDataProvider, getUseField, UseField, + UseMultiFields, + FormDataProvider, useForm, FormSchema, } from '../../../../shared_imports'; @@ -64,6 +66,10 @@ const stepDefineDefaultValue: DefineStepRule = { filters: [], saved_id: undefined, }, + threshold: { + field: [], + value: '200', + }, timeline: { id: null, title: DEFAULT_TIMELINE_TITLE, @@ -84,6 +90,12 @@ MyLabelButton.defaultProps = { flush: 'right', }; +const RuleTypeEuiFormRow = styled(EuiFormRow).attrs<{ $isVisible: boolean }>(({ $isVisible }) => ({ + style: { + display: $isVisible ? 'flex' : 'none', + }, +}))<{ $isVisible: boolean }>``; + const StepDefineRuleComponent: FC = ({ addPadding = false, defaultValues, @@ -97,7 +109,9 @@ const StepDefineRuleComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); - const [localIsMlRule, setIsMlRule] = useState(false); + const [localRuleType, setLocalRuleType] = useState( + defaultValues?.ruleType || stepDefineDefaultValue.ruleType + ); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [myStepData, setMyStepData] = useState({ ...stepDefineDefaultValue, @@ -156,6 +170,17 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); + const ThresholdInputChildren = useCallback( + ({ thresholdField, thresholdValue }) => ( + + ), + [browserFields] + ); + return isReadOnlyView ? ( = ({ isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} /> - + <> = ({ }} /> - - + + <> = ({ }} /> - + + + <> + + {ThresholdInputChildren} + + + = ({ } else if (!deepEqual(index, indicesConfig) && !indexModified) { setIndexModified(true); } + if (myStepData.index !== index) { + setMyStepData((prevValue) => ({ ...prevValue, index })); + } } - if (isMlRule(ruleType) && !localIsMlRule) { - setIsMlRule(true); - clearErrors(); - } else if (!isMlRule(ruleType) && localIsMlRule) { - setIsMlRule(false); + if (ruleType !== localRuleType) { + setLocalRuleType(ruleType); clearErrors(); } - return null; }} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 190d4484b156b8..67d795ccf90f00 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -172,4 +172,36 @@ export const schema: FormSchema = { } ), }, + threshold: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel', + { + defaultMessage: 'Threshold', + } + ), + field: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel', + { + defaultMessage: 'Field', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText', + { + defaultMessage: 'Select a field to group results by', + } + ), + }, + value: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel', + { + defaultMessage: 'Threshold', + } + ), + }, + }, }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx new file mode 100644 index 00000000000000..81e771ce4dc5b6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx @@ -0,0 +1,80 @@ +/* + * 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 React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { getCategorizedFieldNames } from '../../../../timelines/components/edit_data_provider/helpers'; +import { FieldHook, Field } from '../../../../shared_imports'; +import { THRESHOLD_FIELD_PLACEHOLDER } from './translations'; + +const FIELD_COMBO_BOX_WIDTH = 410; + +export interface FieldValueThreshold { + field: string[]; + value: string; +} + +interface ThresholdInputProps { + thresholdField: FieldHook; + thresholdValue: FieldHook; + browserFields: BrowserFields; +} + +const OperatorWrapper = styled(EuiFlexItem)` + align-self: center; +`; + +const fieldDescribedByIds = ['detectionEngineStepDefineRuleThresholdField']; +const valueDescribedByIds = ['detectionEngineStepDefineRuleThresholdValue']; + +const ThresholdInputComponent: React.FC = ({ + thresholdField, + thresholdValue, + browserFields, +}: ThresholdInputProps) => { + const fieldEuiFieldProps = useMemo( + () => ({ + fullWidth: true, + singleSelection: { asPlainText: true }, + noSuggestions: false, + options: getCategorizedFieldNames(browserFields), + placeholder: THRESHOLD_FIELD_PLACEHOLDER, + onCreateOption: undefined, + style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + }), + [browserFields] + ); + + return ( + + + + + {'>='} + + + + + ); +}; + +export const ThresholdInput = React.memo(ThresholdInputComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.ts new file mode 100644 index 00000000000000..228848ef121300 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.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 { i18n } from '@kbn/i18n'; + +export const THRESHOLD_FIELD_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText', + { + defaultMessage: 'All results', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 5c876625cf9f93..1f75ff0210bd51 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -16,6 +16,7 @@ import { rule_name_override, severity_mapping, timestamp_override, + threshold, } from '../../../../../common/detection_engine/schemas/common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { @@ -65,6 +66,7 @@ export const NewRuleSchema = t.intersection([ saved_id: t.string, tags: t.array(t.string), threat: t.array(t.unknown), + threshold, throttle: t.union([t.string, t.null]), to: t.string, updated_by: t.string, @@ -142,6 +144,7 @@ export const RuleSchema = t.intersection([ saved_id: t.string, status: t.string, status_date: t.string, + threshold, timeline_id: t.string, timeline_title: t.string, timestamp_override, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 2b86abf4255c62..5d84cf53140295 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -153,6 +153,10 @@ export const mockRuleWithEverything = (id: string): Rule => ({ ], }, ], + threshold: { + field: 'host.name', + value: 50, + }, throttle: 'no_actions', timestamp_override: 'event.ingested', note: '# this is some markdown documentation', @@ -213,6 +217,10 @@ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Titled timeline', }, + threshold: { + field: [''], + value: '100', + }, }); export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 8331346b19ac9b..4bb7196e17db57 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -51,19 +51,29 @@ export interface RuleFields { queryBar: unknown; index: unknown; ruleType: unknown; + threshold?: unknown; } -type QueryRuleFields = Omit; +type QueryRuleFields = Omit; +type ThresholdRuleFields = Omit; type MlRuleFields = Omit; -const isMlFields = (fields: QueryRuleFields | MlRuleFields): fields is MlRuleFields => - has('anomalyThreshold', fields); +const isMlFields = ( + fields: QueryRuleFields | MlRuleFields | ThresholdRuleFields +): fields is MlRuleFields => has('anomalyThreshold', fields); + +const isThresholdFields = ( + fields: QueryRuleFields | MlRuleFields | ThresholdRuleFields +): fields is ThresholdRuleFields => has('threshold', fields); export const filterRuleFieldsForType = (fields: T, type: RuleType) => { if (isMlRule(type)) { const { index, queryBar, ...mlRuleFields } = fields; return mlRuleFields; + } else if (type === 'threshold') { + const { anomalyThreshold, machineLearningJobId, ...thresholdRuleFields } = fields; + return thresholdRuleFields; } else { - const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + const { anomalyThreshold, machineLearningJobId, threshold, ...queryRuleFields } = fields; return queryRuleFields; } }; @@ -85,6 +95,20 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep anomaly_threshold: ruleFields.anomalyThreshold, machine_learning_job_id: ruleFields.machineLearningJobId, } + : isThresholdFields(ruleFields) + ? { + index: ruleFields.index, + filters: ruleFields.queryBar?.filters, + language: ruleFields.queryBar?.query?.language, + query: ruleFields.queryBar?.query?.query as string, + saved_id: ruleFields.queryBar?.saved_id, + ...(ruleType === 'threshold' && { + threshold: { + field: ruleFields.threshold?.field[0] ?? '', + value: parseInt(ruleFields.threshold?.value, 10) ?? 0, + }, + }), + } : { index: ruleFields.index, filters: ruleFields.queryBar?.filters, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index f8969f06c8ef63..590643f8236eea 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -74,6 +74,10 @@ describe('rule helpers', () => { ], saved_id: 'test123', }, + threshold: { + field: ['host.name'], + value: '50', + }, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Titled timeline', @@ -206,6 +210,10 @@ describe('rule helpers', () => { filters: [], saved_id: "Garrett's IP", }, + threshold: { + field: [], + value: '100', + }, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Untitled timeline', @@ -235,6 +243,10 @@ describe('rule helpers', () => { filters: [], saved_id: undefined, }, + threshold: { + field: [], + value: '100', + }, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Untitled timeline', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index ce37b02a0b5ae3..6541b92f575c1c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -84,6 +84,10 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ id: rule.timeline_id ?? null, title: rule.timeline_title ?? null, }, + threshold: { + field: rule.threshold?.field ? [rule.threshold.field] : [], + value: `${rule.threshold?.value || 100}`, + }, }); export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { @@ -290,6 +294,20 @@ export const redirectToDetections = ( hasEncryptionKey === false || needsListsConfiguration; +const getRuleSpecificRuleParamKeys = (ruleType: RuleType) => { + const queryRuleParams = ['index', 'filters', 'language', 'query', 'saved_id']; + + if (isMlRule(ruleType)) { + return ['anomaly_threshold', 'machine_learning_job_id']; + } + + if (ruleType === 'threshold') { + return ['threshold', ...queryRuleParams]; + } + + return queryRuleParams; +}; + export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { const commonRuleParamsKeys = [ 'id', @@ -312,9 +330,7 @@ export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { const ruleParamsKeys = [ ...commonRuleParamsKeys, - ...(isMlRule(ruleType) - ? ['anomaly_threshold', 'machine_learning_job_id'] - : ['index', 'filters', 'language', 'query', 'saved_id']), + ...getRuleSpecificRuleParamKeys(ruleType), ].sort(); return ruleParamsKeys; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index f453b5a95994d0..e7daff0947b0d5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -10,6 +10,7 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; import { FieldValueTimeline } from '../../../components/rules/pick_timeline'; +import { FieldValueThreshold } from '../../../components/rules/threshold_input'; import { Author, BuildingBlockType, @@ -99,6 +100,7 @@ export interface DefineStepRule extends StepRuleData { queryBar: FieldValueQueryBar; ruleType: RuleType; timeline: FieldValueTimeline; + threshold: FieldValueThreshold; } export interface ScheduleStepRule extends StepRuleData { @@ -122,6 +124,10 @@ export interface DefineStepRuleJson { saved_id?: string; query?: string; language?: string; + threshold?: { + field: string; + value: number; + }; timeline_id?: string; timeline_title?: string; type: RuleType; diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index d5fbf4d865ac5d..43c478ff120a08 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -4750,6 +4750,14 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "threshold", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "exceptions_list", "description": "", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 429590ffc3e7dd..084d1a63fec75f 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -1070,6 +1070,8 @@ export interface RuleField { note?: Maybe; + threshold?: Maybe; + exceptions_list?: Maybe; } @@ -5066,6 +5068,10 @@ export namespace GetTimelineQuery { note: Maybe; + type: Maybe; + + threshold: Maybe; + exceptions_list: Maybe; }; diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index fcd23ff9df4d83..5d4579b427f18f 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -18,6 +18,7 @@ export { FormHook, FormSchema, UseField, + UseMultiFields, useForm, ValidationFunc, VALIDATION_TYPES, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 125ba23a5c5a53..c9c8250922161c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -96,7 +96,7 @@ export const Actions = React.memo( data-test-subj="event-actions-container" > {showCheckboxes && ( - + {loadingEventIds.includes(eventId) ? ( @@ -117,7 +117,7 @@ export const Actions = React.memo( )} - + {loading ? ( @@ -137,7 +137,7 @@ export const Actions = React.memo( {!isEventViewer && ( <> - + ( - + ( ...acc, icon: [ ...acc.icon, - - + + ( return grouped.contextMenu.length > 0 ? [ ...grouped.icon, - - + + ( : grouped.icon; }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); + const handlePinClicked = useCallback( + () => + getPinOnClick({ + allowUnpinning: !eventHasNotes(eventIdToNoteIds[id]), + eventId: id, + onPinEvent, + onUnPinEvent, + isEventPinned, + }), + [eventIdToNoteIds, id, isEventPinned, onPinEvent, onUnPinEvent] + ); + return ( ( loadingEventIds={loadingEventIds} noteIds={eventIdToNoteIds[id] || emptyNotes} onEventToggled={onEventToggled} - onPinClicked={getPinOnClick({ - allowUnpinning: !eventHasNotes(eventIdToNoteIds[id]), - eventId: id, - onPinEvent, - onUnPinEvent, - isEventPinned, - })} + onPinClicked={handlePinClicked} showCheckboxes={showCheckboxes} showNotes={showNotes} toggleShowNotes={toggleShowNotes} diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 2624532b78d4db..6c90b39a8e688c 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -210,6 +210,8 @@ export const timelineQuery = gql` to filters note + type + threshold exceptions_list } } diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index f8afbae840d087..5b093a02b65143 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -416,6 +416,7 @@ export const ecsSchema = gql` updated_by: ToStringArray version: ToStringArray note: ToStringArray + threshold: ToAny exceptions_list: ToAny } diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index b44a8f5cceaf17..668266cc67c3a9 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -1072,6 +1072,8 @@ export interface RuleField { note?: Maybe; + threshold?: Maybe; + exceptions_list?: Maybe; } @@ -4939,6 +4941,8 @@ export namespace RuleFieldResolvers { note?: NoteResolver, TypeParent, TContext>; + threshold?: ThresholdResolver, TypeParent, TContext>; + exceptions_list?: ExceptionsListResolver, TypeParent, TContext>; } @@ -5097,6 +5101,11 @@ export namespace RuleFieldResolvers { Parent = RuleField, TContext = SiemContext > = Resolver; + export type ThresholdResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; export type ExceptionsListResolver< R = Maybe, Parent = RuleField, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 9ca102b4375114..29c56e8ed80b1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -394,6 +394,7 @@ export const getResult = (): RuleAlertType => ({ ], }, ], + threshold: undefined, timestampOverride: undefined, references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index d600bae2746d98..7d80a319e9e520 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -174,6 +174,16 @@ } } }, + "threshold": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "float" + } + } + }, "note": { "type": "text" }, @@ -286,6 +296,9 @@ }, "status": { "type": "keyword" + }, + "threshold_count": { + "type": "float" } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 2942413057e375..acd800e54040c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -90,6 +90,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => severity_mapping: severityMapping, tags, threat, + threshold, throttle, timestamp_override: timestampOverride, to, @@ -177,6 +178,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 310a9da56282d0..edad3dd8a4f213 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -44,6 +44,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } + const { actions: actionsRest, anomaly_threshold: anomalyThreshold, @@ -75,6 +76,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void severity_mapping: severityMapping, tags, threat, + threshold, throttle, timestamp_override: timestampOverride, to, @@ -125,6 +127,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }); } } + const createdRule = await createRules({ alertsClient, anomalyThreshold, @@ -159,6 +162,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 43aa1ecd31922f..18eea7c45585fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -161,6 +161,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP severity_mapping: severityMapping, tags, threat, + threshold, timestamp_override: timestampOverride, to, type, @@ -222,6 +223,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP to, type, threat, + threshold, timestampOverride, references, note, @@ -264,6 +266,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP to, type, threat, + threshold, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index c3d6f920e47a92..5099cf5de958fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -85,6 +85,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestamp_override: timestampOverride, throttle, references, @@ -143,6 +144,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index eb9624e6412e9c..3b3efd2ed166dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -76,6 +76,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, timestamp_override: timestampOverride, throttle, references, @@ -142,6 +143,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index c1ab1be2dbd0a6..518024387fed31 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -88,6 +88,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, throttle, timestamp_override: timestampOverride, references, @@ -156,6 +157,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 717f388cfc1e9d..299b99c4d37b02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -78,6 +78,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, throttle, timestamp_override: timestampOverride, references, @@ -146,6 +147,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 9e93dc051a0413..ee83ea91578c5b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -144,6 +144,7 @@ export const transformAlertToRule = ( to: alert.params.to, type: alert.params.type, threat: alert.params.threat ?? [], + threshold: alert.params.threshold, throttle: ruleActions?.ruleThrottle || 'no_actions', timestamp_override: alert.params.timestampOverride, note: alert.params.note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index a7e24a1ac16096..1117f34b6f8c5a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -39,6 +39,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'query', @@ -81,6 +82,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'machine_learning', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index fd9e87e65d10de..ad4038b05dbd3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -42,6 +42,7 @@ export const createRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -84,6 +85,7 @@ export const createRules = async ({ severity, severityMapping, threat, + threshold, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index 8a86a0f103371e..3af0c3f55b485b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -47,6 +47,7 @@ export const installPrepackagedRules = ( to, type, threat, + threshold, timestamp_override: timestampOverride, references, note, @@ -92,6 +93,7 @@ export const installPrepackagedRules = ( to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index f3102a5ad2cf37..cfb40056eb85d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -143,6 +143,7 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'query', @@ -185,6 +186,7 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'machine_learning', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 577d8d426b63d4..e0814647b4c39a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -42,6 +42,7 @@ export const patchRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -83,6 +84,7 @@ export const patchRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -121,6 +123,7 @@ export const patchRules = async ({ severity, severityMapping, threat, + threshold, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 7b793ffbdb3629..b845990fd94ef9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -59,6 +59,7 @@ import { TagsOrUndefined, ToOrUndefined, ThreatOrUndefined, + ThresholdOrUndefined, TypeOrUndefined, ReferencesOrUndefined, PerPageOrUndefined, @@ -204,6 +205,7 @@ export interface CreateRulesOptions { severityMapping: SeverityMapping; tags: Tags; threat: Threat; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; @@ -247,6 +249,7 @@ export interface UpdateRulesOptions { severityMapping: SeverityMapping; tags: Tags; threat: Threat; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; @@ -288,6 +291,7 @@ export interface PatchRulesOptions { severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; threat: ThreatOrUndefined; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: ToOrUndefined; type: TypeOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index 6466cc596d8915..bf97784e8d917e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -45,6 +45,7 @@ export const updatePrepackagedRules = async ( to, type, threat, + threshold, timestamp_override: timestampOverride, references, version, @@ -93,6 +94,7 @@ export const updatePrepackagedRules = async ( to, type, threat, + threshold, references, version, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index fdc0a61274e759..650b59fb85bc03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -41,6 +41,7 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'query', @@ -84,6 +85,7 @@ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'machine_learning', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 669b70aca4c9de..494a4e221d8629 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -43,6 +43,7 @@ export const updateRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -85,6 +86,7 @@ export const updateRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -129,6 +131,7 @@ export const updateRules = async ({ severity, severityMapping, threat, + threshold, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index aa0512678b073c..17505a44782618 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -53,6 +53,7 @@ describe('utils', () => { severityMapping: undefined, tags: undefined, threat: undefined, + threshold: undefined, to: undefined, timestampOverride: undefined, type: undefined, @@ -94,6 +95,7 @@ describe('utils', () => { severityMapping: undefined, tags: undefined, threat: undefined, + threshold: undefined, to: undefined, timestampOverride: undefined, type: undefined, @@ -135,6 +137,7 @@ describe('utils', () => { severityMapping: undefined, tags: undefined, threat: undefined, + threshold: undefined, to: undefined, timestampOverride: undefined, type: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 861d02a8203e6b..49c02f92ff3361 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -29,6 +29,7 @@ import { TagsOrUndefined, ToOrUndefined, ThreatOrUndefined, + ThresholdOrUndefined, TypeOrUndefined, ReferencesOrUndefined, AuthorOrUndefined, @@ -82,6 +83,7 @@ export interface UpdateProperties { severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; threat: ThreatOrUndefined; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: ToOrUndefined; type: TypeOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 7492422968062b..17e05109b9a879 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -46,6 +46,7 @@ export const sampleRuleAlertParams = ( machineLearningJobId: undefined, filters: undefined, savedId: undefined, + threshold: undefined, timelineId: undefined, timelineTitle: undefined, timestampOverride: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index b368c8fe360542..452ba958876d69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -291,4 +291,158 @@ describe('create_signals', () => { }, }); }); + test('if aggregations is not provided it should not be included', () => { + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortId: undefined, + }); + expect(query).toEqual({ + allowNoIndices: true, + index: ['auditbeat-*'], + size: 100, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 'now-5m', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 'today', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + }); + }); + + test('if aggregations is provided it should be included', () => { + const query = buildEventsSearchQuery({ + aggregations: { + tags: { + terms: { + field: 'tag', + }, + }, + }, + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortId: undefined, + }); + expect(query).toEqual({ + allowNoIndices: true, + index: ['auditbeat-*'], + size: 100, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 'now-5m', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 'today', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + aggregations: { + tags: { + terms: { + field: 'tag', + }, + }, + }, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index c75dddf896fd17..dcf3a90364a401 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -5,6 +5,7 @@ */ interface BuildEventsSearchQuery { + aggregations?: unknown; index: string[]; from: string; to: string; @@ -14,6 +15,7 @@ interface BuildEventsSearchQuery { } export const buildEventsSearchQuery = ({ + aggregations, index, from, to, @@ -74,6 +76,7 @@ export const buildEventsSearchQuery = ({ ], }, }, + ...(aggregations ? { aggregations } : {}), sort: [ { '@timestamp': { @@ -83,6 +86,7 @@ export const buildEventsSearchQuery = ({ ], }, }; + if (searchAfterSortId) { return { ...searchQuery, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index fc8b26450c8522..9e118f77a73e79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -83,5 +83,6 @@ export const buildRule = ({ exceptions_list: ruleParams.exceptionsList ?? [], machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, + threshold: ruleParams.threshold, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index 77a63c63ff97ad..e7098c015c1654 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -46,7 +46,7 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial): S const ruleWithoutInternalTags = removeInternalTagsFromRule(rule); const parent = buildAncestor(doc, rule); const ancestors = buildAncestorsSignal(doc, rule); - const signal: Signal = { + let signal: Signal = { parent, ancestors, original_time: doc._source['@timestamp'], @@ -54,7 +54,11 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial): S rule: ruleWithoutInternalTags, }; if (doc._source.event != null) { - return { ...signal, original_event: doc._source.event }; + signal = { ...signal, original_event: doc._source.event }; + } + if (doc._source.threshold_count != null) { + signal = { ...signal, threshold_count: doc._source.threshold_count }; + delete doc._source.threshold_count; } return signal; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts new file mode 100644 index 00000000000000..744e2b0c06efeb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts @@ -0,0 +1,196 @@ +/* + * 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 { getThresholdSignalQueryFields } from './bulk_create_threshold_signals'; + +describe('getThresholdSignalQueryFields', () => { + it('should return proper fields for match_phrase filters', () => { + const mockFilters = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.dataset': 'traefik.access', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'traefik.access.entryPointName': 'web-secure', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, + }, + { + match_phrase: { + 'url.domain': 'kibana.siem.estc.dev', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(mockFilters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + 'traefik.access.entryPointName': 'web-secure', + 'url.domain': 'kibana.siem.estc.dev', + }); + }); + + it('should return proper fields object for nested match filters', () => { + const filters = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'event.dataset': 'traefik.access', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(filters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + }); + }); + + it('should return proper object for simple match filters', () => { + const filters = { + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'event.dataset': 'traefik.access', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(filters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + }); + }); + + it('should return proper object for simple match_phrase filters', () => { + const filters = { + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'event.dataset': 'traefik.access', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(filters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts new file mode 100644 index 00000000000000..ef9fbe485b92f9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts @@ -0,0 +1,199 @@ +/* + * 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 uuidv5 from 'uuid/v5'; +import { reduce, get, isEmpty } from 'lodash/fp'; +import set from 'set-value'; + +import { Threshold } from '../../../../common/detection_engine/schemas/common/schemas'; +import { Logger } from '../../../../../../../src/core/server'; +import { AlertServices } from '../../../../../alerts/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { RuleTypeParams, RefreshTypes } from '../types'; +import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; +import { SignalSearchResponse } from './types'; + +// used to generate constant Threshold Signals ID when run with the same params +const NAMESPACE_ID = '0684ec03-7201-4ee0-8ee0-3a3f6b2479b2'; + +interface BulkCreateThresholdSignalsParams { + actions: RuleAlertAction[]; + someResult: SignalSearchResponse; + ruleParams: RuleTypeParams; + services: AlertServices; + inputIndexPattern: string[]; + logger: Logger; + id: string; + filter: unknown; + signalsIndex: string; + name: string; + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; + interval: string; + enabled: boolean; + refresh: RefreshTypes; + tags: string[]; + throttle: string; + startedAt: Date; +} + +interface FilterObject { + bool?: { + filter?: FilterObject | FilterObject[]; + should?: Array>>; + }; +} + +const getNestedQueryFilters = (filtersObj: FilterObject): Record => { + if (Array.isArray(filtersObj.bool?.filter)) { + return reduce( + (acc, filterItem) => { + const nestedFilter = getNestedQueryFilters(filterItem); + + if (nestedFilter) { + return { ...acc, ...nestedFilter }; + } + + return acc; + }, + {}, + filtersObj.bool?.filter + ); + } else { + return ( + (filtersObj.bool?.should && + filtersObj.bool?.should[0] && + (filtersObj.bool.should[0].match || filtersObj.bool.should[0].match_phrase)) ?? + {} + ); + } +}; + +export const getThresholdSignalQueryFields = (filter: unknown) => { + const filters = get('bool.filter', filter); + + return reduce( + (acc, item) => { + if (item.match_phrase) { + return { ...acc, ...item.match_phrase }; + } + + if (item.bool.should && (item.bool.should[0].match || item.bool.should[0].match_phrase)) { + return { ...acc, ...(item.bool.should[0].match || item.bool.should[0].match_phrase) }; + } + + if (item.bool?.filter) { + return { ...acc, ...getNestedQueryFilters(item) }; + } + + return acc; + }, + {}, + filters + ); +}; + +const getTransformedHits = ( + results: SignalSearchResponse, + inputIndex: string, + startedAt: Date, + threshold: Threshold, + ruleId: string, + signalQueryFields: Record +) => { + if (isEmpty(threshold.field)) { + const totalResults = + typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value; + + if (totalResults < threshold.value) { + return []; + } + + const source = { + '@timestamp': new Date().toISOString(), + threshold_count: totalResults, + ...signalQueryFields, + }; + + return [ + { + _index: inputIndex, + _id: uuidv5(`${ruleId}${startedAt}${threshold.field}`, NAMESPACE_ID), + _source: source, + }, + ]; + } + + if (!results.aggregations?.threshold) { + return []; + } + + return results.aggregations.threshold.buckets.map( + ({ key, doc_count }: { key: string; doc_count: number }) => { + const source = { + '@timestamp': new Date().toISOString(), + threshold_count: doc_count, + ...signalQueryFields, + }; + + set(source, threshold.field, key); + + return { + _index: inputIndex, + _id: uuidv5(`${ruleId}${startedAt}${threshold.field}${key}`, NAMESPACE_ID), + _source: source, + }; + } + ); +}; + +export const transformThresholdResultsToEcs = ( + results: SignalSearchResponse, + inputIndex: string, + startedAt: Date, + filter: unknown, + threshold: Threshold, + ruleId: string +): SignalSearchResponse => { + const signalQueryFields = getThresholdSignalQueryFields(filter); + const transformedHits = getTransformedHits( + results, + inputIndex, + startedAt, + threshold, + ruleId, + signalQueryFields + ); + const thresholdResults = { + ...results, + hits: { + ...results.hits, + hits: transformedHits, + }, + }; + + set(thresholdResults, 'results.hits.total', transformedHits.length); + + return thresholdResults; +}; + +export const bulkCreateThresholdSignals = async ( + params: BulkCreateThresholdSignalsParams +): Promise => { + const thresholdResults = params.someResult; + const ecsResults = transformThresholdResultsToEcs( + thresholdResults, + params.inputIndexPattern.join(','), + params.startedAt, + params.filter, + params.ruleParams.threshold!, + params.ruleParams.ruleId + ); + + return singleBulkCreate({ ...params, filteredEvents: ecsResults }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts new file mode 100644 index 00000000000000..a9a199f210da0f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -0,0 +1,61 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { Threshold } from '../../../../common/detection_engine/schemas/common/schemas'; +import { singleSearchAfter } from './single_search_after'; + +import { AlertServices } from '../../../../../alerts/server'; +import { Logger } from '../../../../../../../src/core/server'; +import { SignalSearchResponse } from './types'; + +interface FindThresholdSignalsParams { + from: string; + to: string; + inputIndexPattern: string[]; + services: AlertServices; + logger: Logger; + filter: unknown; + threshold: Threshold; +} + +export const findThresholdSignals = async ({ + from, + to, + inputIndexPattern, + services, + logger, + filter, + threshold, +}: FindThresholdSignalsParams): Promise<{ + searchResult: SignalSearchResponse; + searchDuration: string; +}> => { + const aggregations = + threshold && !isEmpty(threshold.field) + ? { + threshold: { + terms: { + field: threshold.field, + min_doc_count: threshold.value, + }, + }, + } + : {}; + + return singleSearchAfter({ + aggregations, + searchAfterSortId: undefined, + index: inputIndexPattern, + from, + to, + services, + logger, + filter, + pageSize: 0, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 4bd9de734f4480..67dc1d50eefcdb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -49,49 +49,62 @@ export const getFilter = async ({ query, lists, }: GetFilterArgs): Promise => { + const queryFilter = () => { + if (query != null && language != null && index != null) { + return getQueryFilter(query, language, filters || [], index, lists); + } else { + throw new BadRequestError('query, filters, and index parameter should be defined'); + } + }; + + const savedQueryFilter = async () => { + if (savedId != null && index != null) { + try { + // try to get the saved object first + const savedObject = await services.savedObjectsClient.get( + 'query', + savedId + ); + return getQueryFilter( + savedObject.attributes.query.query, + savedObject.attributes.query.language, + savedObject.attributes.filters, + index, + lists + ); + } catch (err) { + // saved object does not exist, so try and fall back if the user pushed + // any additional language, query, filters, etc... + if (query != null && language != null && index != null) { + return getQueryFilter(query, language, filters || [], index, lists); + } else { + // user did not give any additional fall back mechanism for generating a rule + // rethrow error for activity monitoring + throw err; + } + } + } else { + throw new BadRequestError('savedId parameter should be defined'); + } + }; + switch (type) { + case 'threshold': { + return savedId != null ? savedQueryFilter() : queryFilter(); + } case 'query': { - if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index, lists); - } else { - throw new BadRequestError('query, filters, and index parameter should be defined'); - } + return queryFilter(); } case 'saved_query': { - if (savedId != null && index != null) { - try { - // try to get the saved object first - const savedObject = await services.savedObjectsClient.get( - 'query', - savedId - ); - return getQueryFilter( - savedObject.attributes.query.query, - savedObject.attributes.query.language, - savedObject.attributes.filters, - index, - lists - ); - } catch (err) { - // saved object does not exist, so try and fall back if the user pushed - // any additional language, query, filters, etc... - if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index, lists); - } else { - // user did not give any additional fall back mechanism for generating a rule - // rethrow error for activity monitoring - throw err; - } - } - } else { - throw new BadRequestError('savedId parameter should be defined'); - } + return savedQueryFilter(); } case 'machine_learning': { throw new BadRequestError( 'Unsupported Rule of type "machine_learning" supplied to getFilter' ); } + default: { + return assertUnreachable(type); + } } - return assertUnreachable(type); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index 2583cf2c8da912..d08ca90f3e3534 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -37,6 +37,9 @@ const signalSchema = schema.object({ severity: schema.string(), severityMapping: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + threshold: schema.maybe( + schema.object({ field: schema.nullable(schema.string()), value: schema.number() }) + ), timestampOverride: schema.nullable(schema.string()), to: schema.string(), type: schema.string(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 49945134e378bb..49efc30b9704d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -26,7 +26,9 @@ import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; +import { findThresholdSignals } from './find_threshold_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; +import { bulkCreateThresholdSignals } from './bulk_create_threshold_signals'; import { scheduleNotificationActions, NotificationRuleTypeParams, @@ -58,6 +60,7 @@ export const signalRulesAlertType = ({ producer: SERVER_APP_ID, async executor({ previousStartedAt, + startedAt, alertId, services, params, @@ -78,6 +81,7 @@ export const signalRulesAlertType = ({ savedId, query, to, + threshold, type, exceptionsList, } = params; @@ -224,6 +228,60 @@ export const signalRulesAlertType = ({ if (bulkCreateDuration) { result.bulkCreateTimes.push(bulkCreateDuration); } + } else if (type === 'threshold' && threshold) { + const inputIndex = await getInputIndex(services, version, index); + const esFilter = await getFilter({ + type, + filters, + language, + query, + savedId, + services, + index: inputIndex, + lists: exceptionItems ?? [], + }); + + const { searchResult: thresholdResults } = await findThresholdSignals({ + inputIndexPattern: inputIndex, + from, + to, + services, + logger, + filter: esFilter, + threshold, + }); + + const { + success, + bulkCreateDuration, + createdItemsCount, + } = await bulkCreateThresholdSignals({ + actions, + throttle, + someResult: thresholdResults, + ruleParams: params, + filter: esFilter, + services, + logger, + id: alertId, + inputIndexPattern: inputIndex, + signalsIndex: outputIndex, + startedAt, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + refresh, + tags, + }); + result.success = success; + result.createdSignalsCount = createdItemsCount; + if (bulkCreateDuration) { + result.bulkCreateTimes.push(bulkCreateDuration); + } } else { const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 409f374d7df1e6..daea277f143682 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -12,6 +12,7 @@ import { buildEventsSearchQuery } from './build_events_query'; import { makeFloatString } from './utils'; interface SingleSearchAfterParams { + aggregations?: unknown; searchAfterSortId: string | undefined; index: string[]; from: string; @@ -24,6 +25,7 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ + aggregations, searchAfterSortId, index, from, @@ -38,6 +40,7 @@ export const singleSearchAfter = async ({ }> => { try { const searchAfterQuery = buildEventsSearchQuery({ + aggregations, index, from, to, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 082211df28320d..5d6bafc5a6d09f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -121,6 +121,7 @@ export interface Signal { original_time: string; original_event?: SearchTypes; status: Status; + threshold_count?: SearchTypes; } export interface SignalHit { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 365222d62d3224..4b4f5147c9a42c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -9,6 +9,7 @@ import { Description, NoteOrUndefined, ThreatOrUndefined, + ThresholdOrUndefined, FalsePositives, From, Immutable, @@ -71,6 +72,7 @@ export interface RuleTypeParams { severity: Severity; severityMapping: SeverityMappingOrUndefined; threat: ThreatOrUndefined; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: RuleType; diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts index 17ed6d20db29eb..19b16bd4bc6d24 100644 --- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts @@ -322,6 +322,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.updated_by': 'signal.rule.updated_by', 'signal.rule.version': 'signal.rule.version', 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', };