From 3e85170549a37381f5aed349ee71699c235bc1e6 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 12 Aug 2021 02:42:13 -0400 Subject: [PATCH] [Security Solution][RAC] - Add reason field (#107532) (#108319) # Conflicts: # x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts --- .../detection_alerts/alerts_details.spec.ts | 2 +- .../detection_rules/custom_query_rule.spec.ts | 4 - .../event_correlation_rule.spec.ts | 6 -- .../indicator_match_rule.spec.ts | 4 - .../detection_rules/override.spec.ts | 4 - .../detection_rules/threshold_rule.spec.ts | 4 - .../security_solution_detections/columns.ts | 30 ++----- .../get_signals_template.test.ts.snap | 14 ++++ .../routes/index/signal_aad_mapping.json | 2 + .../routes/index/signal_extra_fields.json | 3 + .../routes/index/signals_mapping.json | 6 ++ .../factories/utils/build_alert.test.ts | 9 ++- .../rule_types/factories/utils/build_alert.ts | 5 +- .../factories/utils/build_bulk_body.ts | 11 ++- .../rule_types/factories/wrap_hits_factory.ts | 11 ++- .../rule_types/field_maps/alerts.ts | 5 ++ .../signals/build_bulk_body.test.ts | 56 ++++++++++--- .../signals/build_bulk_body.ts | 36 ++++++--- .../signals/build_signal.test.ts | 9 ++- .../detection_engine/signals/build_signal.ts | 3 +- .../signals/bulk_create_ml_signals.ts | 3 +- .../detection_engine/signals/executors/eql.ts | 5 +- .../signals/executors/query.ts | 2 + .../signals/reason_formatter.test.ts | 78 +++++++++++++++++++ .../signals/reason_formatters.ts | 73 +++++++++++++++++ .../signals/search_after_bulk_create.test.ts | 16 ++++ .../signals/search_after_bulk_create.ts | 3 +- .../threat_mapping/create_threat_signal.ts | 2 + .../bulk_create_threshold_signals.ts | 5 +- .../lib/detection_engine/signals/types.ts | 13 +++- .../signals/wrap_hits_factory.ts | 4 +- .../signals/wrap_sequences_factory.ts | 10 ++- .../timelines/common/ecs/ecs_fields/index.ts | 1 + .../public/components/t_grid/body/helpers.tsx | 1 + .../timeline/factory/events/all/constants.ts | 1 + .../security_and_spaces/tests/create_ml.ts | 1 + .../tests/create_threat_matching.ts | 1 + .../tests/generating_signals.ts | 26 +++++-- 38 files changed, 373 insertions(+), 96 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index adeae0e0843df..7d72e759c9f1a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -54,7 +54,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 89, + row: 91, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 7d833b134ddd7..a6043123ce0a8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -14,11 +14,9 @@ import { getNewOverrideRule, } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; @@ -223,8 +221,6 @@ describe('Custom detection rules creation', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.gte(1)); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 677a9b5546494..e06026ce12c7c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -9,11 +9,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; import { @@ -169,8 +167,6 @@ describe('Detection rules, EQL', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); @@ -221,8 +217,6 @@ describe('Detection rules, sequence EQL', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfSequenceAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 07b40df53e2d5..ff000c105a1b4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -9,11 +9,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getIndexPatterns, getNewThreatIndicatorRule } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; import { @@ -482,8 +480,6 @@ describe('indicator match', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', getNewThreatIndicatorRule().name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); cy.get(ALERT_RULE_SEVERITY) .first() .should('have.text', getNewThreatIndicatorRule().severity.toLowerCase()); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 24a56dd563e17..24c98aaee8f97 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -16,10 +16,8 @@ import { import { NUMBER_OF_ALERTS, ALERT_RULE_NAME, - ALERT_RULE_METHOD, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, } from '../../screens/alerts'; import { @@ -196,8 +194,6 @@ describe('Detection rules, override', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.gte(1)); cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat'); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', 'critical'); sortRiskScore(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index dba12fb4ab95c..665df89435952 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -14,11 +14,9 @@ import { } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; @@ -179,8 +177,6 @@ describe('Detection rules, threshold', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index d6d3d829d3be5..89de83ab6e5cf 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -35,18 +35,6 @@ export const columns: Array< initialWidth: DEFAULT_COLUMN_MIN_WIDTH, linkField: 'signal.rule.id', }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_VERSION, - id: 'signal.rule.version', - initialWidth: 95, - }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_METHOD, - id: 'signal.rule.type', - initialWidth: 100, - }, { columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_SEVERITY, @@ -57,31 +45,29 @@ export const columns: Array< columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_RISK_SCORE, id: 'signal.rule.risk_score', - initialWidth: 115, + initialWidth: 100, }, { columnHeaderType: defaultColumnHeaderType, - id: 'event.module', - linkField: 'rule.reference', + displayAsText: i18n.ALERTS_HEADERS_REASON, + id: 'signal.reason', + initialWidth: 450, }, { - aggregatable: true, - category: 'event', columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - type: 'string', + id: 'host.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'event.category', + id: 'user.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'host.name', + id: 'process.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'user.name', + id: 'file.name', }, { columnHeaderType: defaultColumnHeaderType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 9fd3e20f79b43..f07bed9fa556a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -1639,6 +1639,10 @@ Object { "path": "signal.original_event.provider", "type": "alias", }, + "kibana.alert.original_event.reason": Object { + "path": "signal.original_event.reason", + "type": "alias", + }, "kibana.alert.original_event.risk_score": Object { "path": "signal.original_event.risk_score", "type": "alias", @@ -1671,6 +1675,10 @@ Object { "path": "signal.original_time", "type": "alias", }, + "kibana.alert.reason": Object { + "path": "signal.reason", + "type": "alias", + }, "kibana.alert.risk_score": Object { "path": "signal.rule.risk_score", "type": "alias", @@ -3249,6 +3257,9 @@ Object { "provider": Object { "type": "keyword", }, + "reason": Object { + "type": "keyword", + }, "risk_score": Object { "type": "float", }, @@ -3318,6 +3329,9 @@ Object { }, }, }, + "reason": Object { + "type": "keyword", + }, "rule": Object { "properties": Object { "author": Object { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json index 066fdbc87f906..68c184b66c562 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json @@ -17,6 +17,7 @@ "signal.original_event.module": "kibana.alert.original_event.module", "signal.original_event.outcome": "kibana.alert.original_event.outcome", "signal.original_event.provider": "kibana.alert.original_event.provider", + "signal.original_event.reason": "kibana.alert.original_event.reason", "signal.original_event.risk_score": "kibana.alert.original_event.risk_score", "signal.original_event.risk_score_norm": "kibana.alert.original_event.risk_score_norm", "signal.original_event.sequence": "kibana.alert.original_event.sequence", @@ -25,6 +26,7 @@ "signal.original_event.timezone": "kibana.alert.original_event.timezone", "signal.original_event.type": "kibana.alert.original_event.type", "signal.original_time": "kibana.alert.original_time", + "signal.reason": "kibana.alert.reason", "signal.rule.author": "kibana.alert.rule.author", "signal.rule.building_block_type": "kibana.alert.rule.building_block_type", "signal.rule.created_at": "kibana.alert.rule.created_at", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json index e20aa0ef16df4..7bc20fd540b9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json @@ -43,6 +43,9 @@ } } }, + "reason": { + "type": "keyword" + }, "rule": { "type": "object", "properties": { 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 d6a06848592cc..4f754ecd2d33a 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 @@ -360,6 +360,9 @@ "provider": { "type": "keyword" }, + "reason": { + "type": "keyword" + }, "risk_score": { "type": "float" }, @@ -421,6 +424,9 @@ }, "depth": { "type": "integer" + }, + "reason": { + "type": "keyword" } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index 4c59063d39e60..09f35e279a244 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -6,6 +6,7 @@ */ import { + ALERT_REASON, ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, @@ -50,8 +51,9 @@ describe('buildAlert', () => { const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getRulesSchemaMock(); + const reason = 'alert reasonable reason'; const alert = { - ...buildAlert([doc], rule, SPACE_ID), + ...buildAlert([doc], rule, SPACE_ID, reason), ...additionalAlertFields(doc), }; const timestamp = alert['@timestamp']; @@ -68,6 +70,7 @@ describe('buildAlert', () => { }, ], [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z', + [ALERT_REASON]: 'alert reasonable reason', [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { @@ -119,8 +122,9 @@ describe('buildAlert', () => { module: 'system', }; const rule = getRulesSchemaMock(); + const reason = 'alert reasonable reason'; const alert = { - ...buildAlert([doc], rule, SPACE_ID), + ...buildAlert([doc], rule, SPACE_ID, reason), ...additionalAlertFields(doc), }; const timestamp = alert['@timestamp']; @@ -143,6 +147,7 @@ describe('buildAlert', () => { kind: 'event', module: 'system', }, + [ALERT_REASON]: 'alert reasonable reason', [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index ec667fa50934b..eea85ba26faf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -6,6 +6,7 @@ */ import { + ALERT_REASON, ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, @@ -92,7 +93,8 @@ export const removeClashes = (doc: SimpleHit) => { export const buildAlert = ( docs: SimpleHit[], rule: RulesSchema, - spaceId: string | null | undefined + spaceId: string | null | undefined, + reason: string ): RACAlert => { const removedClashes = docs.map(removeClashes); const parents = removedClashes.map(buildParent); @@ -110,6 +112,7 @@ export const buildAlert = ( [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_DEPTH]: depth, + [ALERT_REASON]: reason, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, rule), } as unknown) as RACAlert; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts index ca5857e0ee395..a67337d3b779d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts @@ -9,6 +9,7 @@ import { SavedObject } from 'src/core/types'; import { BaseHit } from '../../../../../../common/detection_engine/types'; import type { ConfigType } from '../../../../../config'; import { buildRuleWithOverrides, buildRuleWithoutOverrides } from '../../../signals/build_rule'; +import { BuildReasonMessage } from '../../../signals/reason_formatters'; import { getMergeStrategy } from '../../../signals/source_fields_merging/strategies'; import { AlertAttributes, SignalSource, SignalSourceHit } from '../../../signals/types'; import { RACAlert } from '../../types'; @@ -35,19 +36,23 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], - applyOverrides: boolean + applyOverrides: boolean, + buildReasonMessage: BuildReasonMessage ): RACAlert => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}) : buildRuleWithoutOverrides(ruleSO); const filteredSource = filterSource(mergedDoc); + const timestamp = new Date().toISOString(); + + const reason = buildReasonMessage({ mergedDoc, rule, timestamp }); if (isSourceDoc(mergedDoc)) { return { ...filteredSource, - ...buildAlert([mergedDoc], rule, spaceId), + ...buildAlert([mergedDoc], rule, spaceId, reason), ...additionalAlertFields(mergedDoc), - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index 0b00b2f656379..62946c52b7f40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -24,7 +24,7 @@ export const wrapHitsFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; mergeStrategy: ConfigType['alertMergeStrategy']; spaceId: string | null | undefined; -}): WrapHits => (events) => { +}): WrapHits => (events, buildReasonMessage) => { try { const wrappedDocs: WrappedRACAlert[] = events.flatMap((doc) => [ { @@ -35,7 +35,14 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(spaceId, ruleSO, doc as SignalSourceHit, mergeStrategy, true), + _source: buildBulkBody( + spaceId, + ruleSO, + doc as SignalSourceHit, + mergeStrategy, + true, + buildReasonMessage + ), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 7ab998fe16074..1c4b7f03fd73f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -193,6 +193,11 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, + 'kibana.alert.reason': { + type: 'keyword', + array: false, + required: false, + }, 'kibana.alert.threat': { type: 'object', array: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 117dcdf0c18da..206f3ae59d246 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -37,11 +37,13 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -77,6 +79,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -91,6 +94,7 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body with threshold results', () => { const ruleSO = sampleRuleSO(getThresholdRuleParams()); const baseDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); const doc: SignalSourceHit & { _source: Required['_source'] } = { ...baseDoc, _source: { @@ -109,7 +113,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -145,6 +150,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: { ...expectedRule(), @@ -181,6 +187,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { action: 'socket_opened', @@ -191,7 +198,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -227,6 +235,7 @@ describe('buildBulkBody', () => { depth: 0, }, ], + reason: 'reasonable reason', ancestors: [ { id: sampleIdGuid, @@ -250,6 +259,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with but no kind information', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { action: 'socket_opened', @@ -259,7 +269,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -303,6 +314,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -317,6 +329,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with with only kind information', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { kind: 'event', @@ -324,7 +337,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -363,6 +377,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -377,6 +392,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as a numeric', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const sampleDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -388,7 +404,8 @@ describe('buildBulkBody', () => { const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); const expected: Omit & { someKey: string } = { someKey: 'someValue', @@ -423,6 +440,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -437,6 +455,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as an object', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const sampleDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -448,7 +467,8 @@ describe('buildBulkBody', () => { const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); const expected: Omit & { someKey: string } = { someKey: 'someValue', @@ -483,6 +503,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -504,7 +525,12 @@ describe('buildSignalFromSequence', () => { block2._source.new_key = 'new_key_value'; const blocks = [block1, block2]; const ruleSO = sampleRuleSO(getQueryRuleParams()); - const signal: SignalHitOptionalTimestamp = buildSignalFromSequence(blocks, ruleSO); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); + const signal: SignalHitOptionalTimestamp = buildSignalFromSequence( + blocks, + ruleSO, + buildReasonMessage + ); // Timestamp will potentially always be different so remove it for the test delete signal['@timestamp']; const expected: Omit & { new_key: string } = { @@ -573,6 +599,7 @@ describe('buildSignalFromSequence', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, group: { @@ -589,7 +616,12 @@ describe('buildSignalFromSequence', () => { block2._source['@timestamp'] = '2021-05-20T22:28:46+0000'; block2._source.someKey = 'someOtherValue'; const ruleSO = sampleRuleSO(getQueryRuleParams()); - const signal: SignalHitOptionalTimestamp = buildSignalFromSequence([block1, block2], ruleSO); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); + const signal: SignalHitOptionalTimestamp = buildSignalFromSequence( + [block1, block2], + ruleSO, + buildReasonMessage + ); // Timestamp will potentially always be different so remove it for the test delete signal['@timestamp']; const expected: Omit = { @@ -657,6 +689,7 @@ describe('buildSignalFromSequence', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, group: { @@ -673,11 +706,13 @@ describe('buildSignalFromEvent', () => { const ancestor = sampleDocWithAncestors().hits.hits[0]; delete ancestor._source.source; const ruleSO = sampleRuleSO(getQueryRuleParams()); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); const signal: SignalHitOptionalTimestamp = buildSignalFromEvent( ancestor, ruleSO, true, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -724,6 +759,7 @@ describe('buildSignalFromEvent', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 54a41be5cbade..626dcb2fe83ff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -22,6 +22,7 @@ import { buildEventTypeSignal } from './build_event_type_signal'; import { EqlSequence } from '../../../../common/detection_engine/types'; import { generateSignalId, wrapBuildingBlocks, wrapSignal } from './utils'; import type { ConfigType } from '../../../config'; +import { BuildReasonMessage } from './reason_formatters'; /** * Formats the search_after result for insertion into the signals index. We first create a @@ -35,12 +36,15 @@ import type { ConfigType } from '../../../config'; export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): SignalHit => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}); + const timestamp = new Date().toISOString(); + const reason = buildReasonMessage({ mergedDoc, rule, timestamp }); const signal: Signal = { - ...buildSignal([mergedDoc], rule), + ...buildSignal([mergedDoc], rule, reason), ...additionalSignalFields(mergedDoc), }; const event = buildEventTypeSignal(mergedDoc); @@ -52,7 +56,7 @@ export const buildBulkBody = ( }; const signalHit: SignalHit = { ...filteredSource, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event, signal, }; @@ -71,11 +75,12 @@ export const buildSignalGroupFromSequence = ( sequence: EqlSequence, ruleSO: SavedObject, outputIndex: string, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): WrappedSignalHit[] => { const wrappedBuildingBlocks = wrapBuildingBlocks( sequence.events.map((event) => { - const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy); + const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy, buildReasonMessage); signal.signal.rule.building_block_type = 'default'; return signal; }), @@ -94,7 +99,7 @@ export const buildSignalGroupFromSequence = ( // we can build the signal that links the building blocks together // and also insert the group id (which is also the "shell" signal _id) in each building block const sequenceSignal = wrapSignal( - buildSignalFromSequence(wrappedBuildingBlocks, ruleSO), + buildSignalFromSequence(wrappedBuildingBlocks, ruleSO, buildReasonMessage), outputIndex ); wrappedBuildingBlocks.forEach((block, idx) => { @@ -111,14 +116,18 @@ export const buildSignalGroupFromSequence = ( export const buildSignalFromSequence = ( events: WrappedSignalHit[], - ruleSO: SavedObject + ruleSO: SavedObject, + buildReasonMessage: BuildReasonMessage ): SignalHit => { const rule = buildRuleWithoutOverrides(ruleSO); - const signal: Signal = buildSignal(events, rule); + const timestamp = new Date().toISOString(); + + const reason = buildReasonMessage({ rule, timestamp }); + const signal: Signal = buildSignal(events, rule, reason); const mergedEvents = objectArrayIntersection(events.map((event) => event._source)); return { ...mergedEvents, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event: { kind: 'signal', }, @@ -137,14 +146,17 @@ export const buildSignalFromEvent = ( event: BaseSignalHit, ruleSO: SavedObject, applyOverrides: boolean, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): SignalHit => { const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedEvent._source ?? {}) : buildRuleWithoutOverrides(ruleSO); + const timestamp = new Date().toISOString(); + const reason = buildReasonMessage({ mergedDoc: mergedEvent, rule, timestamp }); const signal: Signal = { - ...buildSignal([mergedEvent], rule), + ...buildSignal([mergedEvent], rule, reason), ...additionalSignalFields(mergedEvent), }; const eventFields = buildEventTypeSignal(mergedEvent); @@ -155,7 +167,7 @@ export const buildSignalFromEvent = ( // TODO: better naming for SignalHit - it's really a new signal to be inserted const signalHit: SignalHit = { ...filteredSource, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event: eventFields, signal, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 8c0790761a5e0..90b9cce9e057d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -31,8 +31,10 @@ describe('buildSignal', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getRulesSchemaMock(); + const reason = 'signal reasonable reason'; + const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, reason), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -62,6 +64,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'signal reasonable reason', status: 'open', rule: { author: [], @@ -112,8 +115,9 @@ describe('buildSignal', () => { module: 'system', }; const rule = getRulesSchemaMock(); + const reason = 'signal reasonable reason'; const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, reason), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -143,6 +147,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'signal reasonable reason', original_event: { action: 'socket_opened', dataset: 'socket', 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 a415c83e857c2..962869cc4d61a 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 @@ -77,7 +77,7 @@ export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => { * @param docs The parent signals/events of the new signal to be built. * @param rule The rule that is generating the new signal. */ -export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => { +export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema, reason: string): Signal => { const _meta = { version: SIGNALS_TEMPLATE_VERSION, }; @@ -94,6 +94,7 @@ export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => ancestors, status: 'open', rule, + reason, depth, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index ebb4462817eab..be6f4cb8feae5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -19,6 +19,7 @@ import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; import { AlertAttributes, BulkCreate, WrapHits } from './types'; import { MachineLearningRuleParams } from '../schemas/rule_schemas'; +import { buildReasonMessageForMlAlert } from './reason_formatters'; interface BulkCreateMlSignalsParams { someResult: AnomalyResults; @@ -89,6 +90,6 @@ export const bulkCreateMlSignals = async ( const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - const wrappedDocs = params.wrapHits(ecsResults.hits.hits); + const wrappedDocs = params.wrapHits(ecsResults.hits.hits, buildReasonMessageForMlAlert); return params.bulkCreate(wrappedDocs); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index 8d19510c63477..9a2805610ca8b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -35,6 +35,7 @@ import { } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildReasonMessageForEqlAlert } from '../reason_formatters'; export const eqlExecutor = async ({ rule, @@ -119,9 +120,9 @@ export const eqlExecutor = async ({ result.searchAfterTimes = [eqlSearchDuration]; let newSignals: SimpleHit[] | undefined; if (response.hits.sequences !== undefined) { - newSignals = wrapSequences(response.hits.sequences); + newSignals = wrapSequences(response.hits.sequences, buildReasonMessageForEqlAlert); } else if (response.hits.events !== undefined) { - newSignals = wrapHits(response.hits.events); + newSignals = wrapHits(response.hits.events, buildReasonMessageForEqlAlert); } else { throw new Error( 'eql query response should have either `sequences` or `events` but had neither' diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index f27680315d194..f281475fe59eb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -22,6 +22,7 @@ import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildReasonMessageForQueryAlert } from '../reason_formatters'; export const queryExecutor = async ({ rule, @@ -84,6 +85,7 @@ export const queryExecutor = async ({ signalsIndex: ruleParams.outputIndex, filter: esFilter, pageSize: searchAfterSize, + buildReasonMessage: buildReasonMessageForQueryAlert, buildRuleMessage, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts new file mode 100644 index 0000000000000..e7f4fb41c763b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildCommonReasonMessage } from './reason_formatters'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { SignalSourceHit } from './types'; + +describe('reason_formatter', () => { + let rule: RulesSchema; + let mergedDoc: SignalSourceHit; + let timestamp: string; + beforeAll(() => { + rule = { + name: 'What is in a name', + risk_score: 9000, + severity: 'medium', + } as RulesSchema; // Cast here as all fields aren't required + mergedDoc = { + _index: 'some-index', + _id: 'some-id', + fields: { + 'host.name': ['party host'], + 'user.name': ['ferris bueller'], + '@timestamp': '2021-08-11T02:28:59.101Z', + }, + }; + timestamp = '2021-08-11T02:28:59.401Z'; + }); + + describe('buildCommonReasonMessage', () => { + describe('when rule, mergedDoc, and timestamp are provided', () => { + it('should return the full reason message', () => { + expect(buildCommonReasonMessage({ rule, mergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 by ferris bueller on party host.' + ); + }); + }); + describe('when rule, mergedDoc, and timestamp are provided and host.name is missing', () => { + it('should return the reason message without the host name', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'host.name': ['-'], + }, + }; + expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 by ferris bueller.' + ); + }); + }); + describe('when rule, mergedDoc, and timestamp are provided and user.name is missing', () => { + it('should return the reason message without the user name', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'user.name': ['-'], + }, + }; + expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 on party host.' + ); + }); + }); + describe('when only rule and timestamp are provided', () => { + it('should return the reason message without host name or user name', () => { + expect(buildCommonReasonMessage({ rule, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000.' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts new file mode 100644 index 0000000000000..0586462a2a581 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { SignalSourceHit } from './types'; + +export interface BuildReasonMessageArgs { + rule: RulesSchema; + mergedDoc?: SignalSourceHit; + timestamp: string; +} + +export type BuildReasonMessage = (args: BuildReasonMessageArgs) => string; + +/** + * Currently all security solution rule types share a common reason message string. This function composes that string + * In the future there may be different configurations based on the different rule types, so the plumbing has been put in place + * to more easily allow for this in the future. + * @export buildCommonReasonMessage - is only exported for testing purposes, and only used internally here. + */ +export const buildCommonReasonMessage = ({ + rule, + mergedDoc, + timestamp, +}: BuildReasonMessageArgs) => { + if (!rule) { + // This should never happen, but in case, better to not show a malformed string + return ''; + } + let hostName; + let userName; + if (mergedDoc?.fields) { + hostName = mergedDoc.fields['host.name'] != null ? mergedDoc.fields['host.name'] : hostName; + userName = mergedDoc.fields['user.name'] != null ? mergedDoc.fields['user.name'] : userName; + } + + const isFieldEmpty = (field: string | string[] | undefined | null) => + !field || !field.length || (field.length === 1 && field[0] === '-'); + + return i18n.translate('xpack.securitySolution.detectionEngine.signals.alertReasonDescription', { + defaultMessage: + 'Alert {alertName} created at {timestamp} with a {alertSeverity} severity and risk score of {alertRiskScore}{userName, select, null {} other {{whitespace}by {userName}} }{hostName, select, null {} other {{whitespace}on {hostName}} }.', + values: { + alertName: rule.name, + alertSeverity: rule.severity, + alertRiskScore: rule.risk_score, + hostName: isFieldEmpty(hostName) ? 'null' : hostName, + timestamp, + userName: isFieldEmpty(userName) ? 'null' : userName, + whitespace: ' ', // there isn't support for the unicode /u0020 for whitespace, and leading spaces are deleted, so to prevent double-whitespace explicitly passing the space in. + }, + }); +}; + +export const buildReasonMessageForEqlAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForMlAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForQueryAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForThreatMatchAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForThresholdAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 711db931e9072..8bf0c986b9c25 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -32,11 +32,13 @@ import { bulkCreateFactory } from './bulk_create_factory'; import { wrapHitsFactory } from './wrap_hits_factory'; import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { BuildReasonMessage } from './reason_formatters'; const buildRuleMessage = mockBuildRuleMessage; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; + let buildReasonMessage: BuildReasonMessage; let bulkCreate: BulkCreate; let wrapHits: WrapHits; let inputIndexPattern: string[] = []; @@ -48,6 +50,7 @@ describe('searchAfterAndBulkCreate', () => { let tuple: RuleRangeTuple; beforeEach(() => { jest.clearAllMocks(); + buildReasonMessage = jest.fn().mockResolvedValue('some alert reason message'); listClient = listMock.getListClient(); listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; @@ -191,6 +194,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -295,6 +299,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -373,6 +378,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -432,6 +438,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -511,6 +518,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -566,6 +574,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -638,6 +647,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -712,6 +722,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -763,6 +774,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -810,6 +822,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -871,6 +884,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -997,6 +1011,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -1093,6 +1108,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 7b5b61577cf32..8037a9a201510 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -34,6 +34,7 @@ export const searchAfterAndBulkCreate = async ({ filter, pageSize, buildRuleMessage, + buildReasonMessage, enrichment = identity, bulkCreate, wrapHits, @@ -146,7 +147,7 @@ export const searchAfterAndBulkCreate = async ({ ); } const enrichedEvents = await enrichment(filteredEvents); - const wrappedDocs = wrapHits(enrichedEvents.hits.hits); + const wrappedDocs = wrapHits(enrichedEvents.hits.hits, buildReasonMessage); const { bulkCreateDuration: bulkDuration, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index fb9881b519a16..312d75f7a10cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -9,6 +9,7 @@ import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; +import { buildReasonMessageForThreatMatchAlert } from '../reason_formatters'; import { CreateThreatSignalOptions } from './types'; import { SearchAfterAndBulkCreateReturnType } from '../types'; @@ -83,6 +84,7 @@ export const createThreatSignal = async ({ filter: esFilter, pageSize: searchAfterSize, buildRuleMessage, + buildReasonMessage: buildReasonMessageForThreatMatchAlert, enrichment: threatEnrichment, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index f56ed3a5e9eb4..afb0353c4ba03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -24,6 +24,7 @@ import { getThresholdAggregationParts, getThresholdTermsHash, } from '../utils'; +import { buildReasonMessageForThresholdAlert } from '../reason_formatters'; import type { MultiAggBucket, SignalSource, @@ -248,5 +249,7 @@ export const bulkCreateThresholdSignals = async ( params.thresholdSignalHistory ); - return params.bulkCreate(params.wrapHits(ecsResults.hits.hits)); + return params.bulkCreate( + params.wrapHits(ecsResults.hits.hits, buildReasonMessageForThresholdAlert) + ); }; 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 4da411d0c70a1..89233cf2c8242 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 @@ -35,6 +35,7 @@ import { RuleParams } from '../schemas/rule_schemas'; import { GenericBulkCreateResponse } from './bulk_create_factory'; import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; +import { BuildReasonMessage } from './reason_formatters'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -238,6 +239,7 @@ export interface Signal { }; original_time?: string; original_event?: SearchTypes; + reason?: string; status: Status; threshold_result?: ThresholdResult; original_signal?: SearchTypes; @@ -286,9 +288,15 @@ export type BulkCreate = (docs: Array>) => Promise; -export type WrapHits = (hits: estypes.SearchHit[]) => SimpleHit[]; +export type WrapHits = ( + hits: Array>, + buildReasonMessage: BuildReasonMessage +) => SimpleHit[]; -export type WrapSequences = (sequences: Array>) => SimpleHit[]; +export type WrapSequences = ( + sequences: Array>, + buildReasonMessage: BuildReasonMessage +) => SimpleHit[]; export interface SearchAfterAndBulkCreateParams { tuple: { @@ -308,6 +316,7 @@ export interface SearchAfterAndBulkCreateParams { pageSize: number; filter: unknown; buildRuleMessage: BuildRuleMessage; + buildReasonMessage: BuildReasonMessage; enrichment?: SignalsEnrichment; bulkCreate: BulkCreate; wrapHits: WrapHits; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts index 5cef740e17895..19bdd58140a33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts @@ -24,7 +24,7 @@ export const wrapHitsFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; -}): WrapHits => (events) => { +}): WrapHits => (events, buildReasonMessage) => { const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ { _index: signalsIndex, @@ -34,7 +34,7 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy), + _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy, buildReasonMessage), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts index f0b9e64047692..0ca4b9688f971 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts @@ -17,11 +17,17 @@ export const wrapSequencesFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; -}): WrapSequences => (sequences) => +}): WrapSequences => (sequences, buildReasonMessage) => sequences.reduce( (acc: WrappedSignalHit[], sequence) => [ ...acc, - ...buildSignalGroupFromSequence(sequence, ruleSO, signalsIndex, mergeStrategy), + ...buildSignalGroupFromSequence( + sequence, + ruleSO, + signalsIndex, + mergeStrategy, + buildReasonMessage + ), ], [] ); diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts index 292822019fc9c..239e295a1f8b1 100644 --- a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts +++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts @@ -292,6 +292,7 @@ export const systemFieldsMap: Readonly> = { export const signalFieldsMap: Readonly> = { 'signal.original_time': 'signal.original_time', + 'signal.reason': 'signal.reason', 'signal.rule.id': 'signal.rule.id', 'signal.rule.saved_id': 'signal.rule.saved_id', 'signal.rule.timeline_id': 'signal.rule.timeline_id', diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx index 790414314ecdd..3dea3e71445a1 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -147,6 +147,7 @@ export const allowSorting = ({ 'signal.parent.index', 'signal.parent.rule', 'signal.parent.type', + 'signal.reason', 'signal.rule.created_by', 'signal.rule.description', 'signal.rule.enabled', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts index aae68dbcf86d1..9b45a5bebfc21 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts @@ -45,6 +45,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'signal.status', 'signal.group.id', 'signal.original_time', + 'signal.reason', 'signal.rule.filters', 'signal.rule.from', 'signal.rule.language', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index a03bd07c86020..cd209da25e883 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -193,6 +193,7 @@ export default ({ getService }: FtrProviderContext) => { index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', depth: 0, }, + reason: `Alert Test ML rule created at ${signal._source['@timestamp']} with a critical severity and risk score of 50 by root on mothra.`, original_time: '2020-11-16T22:58:08.000Z', }, all_field_values: [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index c341761160633..399eafc475a89 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -275,6 +275,7 @@ export default ({ getService }: FtrProviderContext) => { depth: 0, }, ], + reason: `Alert Query with a rule id created at ${fullSignal['@timestamp']} with a high severity and risk score of 55 by root on zeek-sensor-amsterdam.`, rule: fullSignal.signal.rule, status: 'open', }, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 66c94a7317b72..1c1e2b9966b7f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -112,7 +112,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -165,7 +166,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -228,7 +230,7 @@ export default ({ getService }: FtrProviderContext) => { const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -360,6 +362,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -494,6 +497,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -658,6 +662,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 by root on zeek-sensor-amsterdam.`, rule: fullSignal.signal.rule, group: fullSignal.signal.group, original_time: fullSignal.signal.original_time, @@ -748,6 +753,7 @@ export default ({ getService }: FtrProviderContext) => { status: 'open', depth: 2, group: source.signal.group, + reason: `Alert Signal Testing Query created at ${source['@timestamp']} with a high severity and risk score of 1.`, rule: source.signal.rule, ancestors: [ { @@ -866,6 +872,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1003,6 +1010,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1086,6 +1094,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1171,7 +1180,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -1228,7 +1238,7 @@ export default ({ getService }: FtrProviderContext) => { const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -1325,7 +1335,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -1387,7 +1398,7 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByRuleIds(supertest, ['signal-on-signal']); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -1675,6 +1686,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert boot created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on zeek-sensor-amsterdam.`, rule: { ...fullSignal.signal.rule, name: 'boot',