Skip to content

Commit 087aaed

Browse files
[Security Solution][Detections] Add rule overrides for single event EQL rules (#78876) (#79033)
* Add buildRuleWithOverrides function for single event EQL queries * Disable rule overrides for all sequence signals Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 3dcf754 commit 087aaed

File tree

13 files changed

+232
-105
lines changed

13 files changed

+232
-105
lines changed

x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_eql.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"from": "now-300m",
2626
"severity": "high",
2727
"type": "eql",
28+
"language": "eql",
2829
"threat": [
2930
{
3031
"framework": "MITRE ATT&CK",

x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { RuleTypeParams } from '../../types';
2222
import { IRuleStatusAttributes } from '../../rules/types';
2323
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
2424
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
25+
import { RulesSchema } from '../../../../../common/detection_engine/schemas/response';
2526

2627
export const sampleRuleAlertParams = (
2728
maxSignals?: number | undefined,
@@ -92,6 +93,46 @@ export const sampleRuleSO = (): SavedObject<RuleAlertAttributes> => {
9293
};
9394
};
9495

96+
export const expectedRule = (): RulesSchema => {
97+
return {
98+
actions: [],
99+
author: ['Elastic'],
100+
building_block_type: 'default',
101+
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
102+
rule_id: 'rule-1',
103+
false_positives: [],
104+
max_signals: 10000,
105+
risk_score: 50,
106+
risk_score_mapping: [],
107+
output_index: '.siem-signals',
108+
description: 'Detecting root and admin users',
109+
from: 'now-6m',
110+
immutable: false,
111+
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
112+
interval: '5m',
113+
language: 'kuery',
114+
license: 'Elastic License',
115+
name: 'rule-name',
116+
query: 'user.name: root or user.name: admin',
117+
references: ['http://google.com'],
118+
severity: 'high',
119+
severity_mapping: [],
120+
tags: ['some fake tag 1', 'some fake tag 2'],
121+
threat: [],
122+
type: 'query',
123+
to: 'now',
124+
note: '',
125+
enabled: true,
126+
created_by: 'sample user',
127+
updated_by: 'sample user',
128+
version: 1,
129+
updated_at: '2020-03-27T22:55:59.577Z',
130+
created_at: '2020-03-27T22:55:59.577Z',
131+
throttle: 'no_actions',
132+
exceptions_list: getListArrayMock(),
133+
};
134+
};
135+
95136
export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): SignalSourceHit => ({
96137
_index: 'myFakeSignalIndex',
97138
_type: 'doc',

x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ describe('buildSignalFromEvent', () => {
546546
const ancestor = sampleDocWithAncestors().hits.hits[0];
547547
delete ancestor._source.source;
548548
const ruleSO = sampleRuleSO();
549-
const signal = buildSignalFromEvent(ancestor, ruleSO);
549+
const signal = buildSignalFromEvent(ancestor, ruleSO, true);
550550
// Timestamp will potentially always be different so remove it for the test
551551
// @ts-expect-error
552552
delete signal['@timestamp'];

x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
BaseSignalHit,
1414
SignalSource,
1515
} from './types';
16-
import { buildRule, buildRuleWithoutOverrides } from './build_rule';
16+
import { buildRule, buildRuleWithoutOverrides, buildRuleWithOverrides } from './build_rule';
1717
import { additionalSignalFields, buildSignal } from './build_signal';
1818
import { buildEventTypeSignal } from './build_event_type_signal';
1919
import { RuleAlertAction } from '../../../../common/detection_engine/types';
@@ -97,7 +97,7 @@ export const buildSignalGroupFromSequence = (
9797
): BaseSignalHit[] => {
9898
const wrappedBuildingBlocks = wrapBuildingBlocks(
9999
sequence.events.map((event) => {
100-
const signal = buildSignalFromEvent(event, ruleSO);
100+
const signal = buildSignalFromEvent(event, ruleSO, false);
101101
signal.signal.rule.building_block_type = 'default';
102102
return signal;
103103
}),
@@ -147,9 +147,12 @@ export const buildSignalFromSequence = (
147147

148148
export const buildSignalFromEvent = (
149149
event: BaseSignalHit,
150-
ruleSO: SavedObject<RuleAlertAttributes>
150+
ruleSO: SavedObject<RuleAlertAttributes>,
151+
applyOverrides: boolean
151152
): SignalHit => {
152-
const rule = buildRuleWithoutOverrides(ruleSO);
153+
const rule = applyOverrides
154+
? buildRuleWithOverrides(ruleSO, event._source)
155+
: buildRuleWithoutOverrides(ruleSO);
153156
const signal = {
154157
...buildSignal([event], rule),
155158
...additionalSignalFields(event),

x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts

Lines changed: 114 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { buildRule, removeInternalTagsFromRule, buildRuleWithoutOverrides } from './build_rule';
7+
import {
8+
buildRule,
9+
removeInternalTagsFromRule,
10+
buildRuleWithOverrides,
11+
buildRuleWithoutOverrides,
12+
} from './build_rule';
813
import {
914
sampleDocNoSortId,
1015
sampleRuleAlertParams,
1116
sampleRuleGuid,
1217
sampleRuleSO,
18+
expectedRule,
19+
sampleDocSeverity,
1320
} from './__mocks__/es_results';
1421
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
1522
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
@@ -312,43 +319,7 @@ describe('buildRuleWithoutOverrides', () => {
312319
test('builds a rule using rule SO', () => {
313320
const ruleSO = sampleRuleSO();
314321
const rule = buildRuleWithoutOverrides(ruleSO);
315-
expect(rule).toEqual({
316-
actions: [],
317-
author: ['Elastic'],
318-
building_block_type: 'default',
319-
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
320-
rule_id: 'rule-1',
321-
false_positives: [],
322-
max_signals: 10000,
323-
risk_score: 50,
324-
risk_score_mapping: [],
325-
output_index: '.siem-signals',
326-
description: 'Detecting root and admin users',
327-
from: 'now-6m',
328-
immutable: false,
329-
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
330-
interval: '5m',
331-
language: 'kuery',
332-
license: 'Elastic License',
333-
name: 'rule-name',
334-
query: 'user.name: root or user.name: admin',
335-
references: ['http://google.com'],
336-
severity: 'high',
337-
severity_mapping: [],
338-
tags: ['some fake tag 1', 'some fake tag 2'],
339-
threat: [],
340-
type: 'query',
341-
to: 'now',
342-
note: '',
343-
enabled: true,
344-
created_by: 'sample user',
345-
updated_by: 'sample user',
346-
version: 1,
347-
updated_at: ruleSO.updated_at ?? '',
348-
created_at: ruleSO.attributes.createdAt,
349-
throttle: 'no_actions',
350-
exceptions_list: getListArrayMock(),
351-
});
322+
expect(rule).toEqual(expectedRule());
352323
});
353324

354325
test('builds a rule using rule SO and removes internal tags', () => {
@@ -360,42 +331,110 @@ describe('buildRuleWithoutOverrides', () => {
360331
`${INTERNAL_IMMUTABLE_KEY}:true`,
361332
];
362333
const rule = buildRuleWithoutOverrides(ruleSO);
363-
expect(rule).toEqual({
364-
actions: [],
365-
author: ['Elastic'],
366-
building_block_type: 'default',
367-
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
368-
rule_id: 'rule-1',
369-
false_positives: [],
370-
max_signals: 10000,
371-
risk_score: 50,
372-
risk_score_mapping: [],
373-
output_index: '.siem-signals',
374-
description: 'Detecting root and admin users',
375-
from: 'now-6m',
376-
immutable: false,
377-
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
378-
interval: '5m',
379-
language: 'kuery',
380-
license: 'Elastic License',
381-
name: 'rule-name',
382-
query: 'user.name: root or user.name: admin',
383-
references: ['http://google.com'],
384-
severity: 'high',
385-
severity_mapping: [],
386-
tags: ['some fake tag 1', 'some fake tag 2'],
387-
threat: [],
388-
type: 'query',
389-
to: 'now',
390-
note: '',
391-
enabled: true,
392-
created_by: 'sample user',
393-
updated_by: 'sample user',
394-
version: 1,
395-
updated_at: ruleSO.updated_at ?? '',
396-
created_at: ruleSO.attributes.createdAt,
397-
throttle: 'no_actions',
398-
exceptions_list: getListArrayMock(),
399-
});
334+
expect(rule).toEqual(expectedRule());
335+
});
336+
});
337+
338+
describe('buildRuleWithOverrides', () => {
339+
beforeEach(() => {
340+
jest.clearAllMocks();
341+
});
342+
343+
test('it builds a rule as expected with filters present', () => {
344+
const ruleSO = sampleRuleSO();
345+
ruleSO.attributes.params.filters = [
346+
{
347+
query: 'host.name: Rebecca',
348+
},
349+
{
350+
query: 'host.name: Evan',
351+
},
352+
{
353+
query: 'host.name: Braden',
354+
},
355+
];
356+
const rule = buildRuleWithOverrides(ruleSO, sampleDocNoSortId()._source);
357+
const expected: RulesSchema = {
358+
...expectedRule(),
359+
filters: ruleSO.attributes.params.filters,
360+
};
361+
expect(rule).toEqual(expected);
362+
});
363+
364+
test('it builds a rule and removes internal tags', () => {
365+
const ruleSO = sampleRuleSO();
366+
ruleSO.attributes.tags = [
367+
'some fake tag 1',
368+
'some fake tag 2',
369+
`${INTERNAL_RULE_ID_KEY}:rule-1`,
370+
`${INTERNAL_IMMUTABLE_KEY}:true`,
371+
];
372+
const rule = buildRuleWithOverrides(ruleSO, sampleDocNoSortId()._source);
373+
expect(rule).toEqual(expectedRule());
374+
});
375+
376+
test('it applies rule name override in buildRule', () => {
377+
const ruleSO = sampleRuleSO();
378+
ruleSO.attributes.params.ruleNameOverride = 'someKey';
379+
const rule = buildRuleWithOverrides(ruleSO, sampleDocNoSortId()._source);
380+
const expected = {
381+
...expectedRule(),
382+
name: 'someValue',
383+
rule_name_override: 'someKey',
384+
meta: {
385+
ruleNameOverridden: true,
386+
},
387+
};
388+
expect(rule).toEqual(expected);
389+
});
390+
391+
test('it applies risk score override in buildRule', () => {
392+
const newRiskScore = 79;
393+
const ruleSO = sampleRuleSO();
394+
ruleSO.attributes.params.riskScoreMapping = [
395+
{
396+
field: 'new_risk_score',
397+
// value and risk_score aren't used for anything but are required in the schema
398+
value: '',
399+
operator: 'equals',
400+
risk_score: undefined,
401+
},
402+
];
403+
const doc = sampleDocNoSortId();
404+
doc._source.new_risk_score = newRiskScore;
405+
const rule = buildRuleWithOverrides(ruleSO, doc._source);
406+
const expected = {
407+
...expectedRule(),
408+
risk_score: newRiskScore,
409+
risk_score_mapping: ruleSO.attributes.params.riskScoreMapping,
410+
meta: {
411+
riskScoreOverridden: true,
412+
},
413+
};
414+
expect(rule).toEqual(expected);
415+
});
416+
417+
test('it applies severity override in buildRule', () => {
418+
const eventSeverity = '42';
419+
const ruleSO = sampleRuleSO();
420+
ruleSO.attributes.params.severityMapping = [
421+
{
422+
field: 'event.severity',
423+
value: eventSeverity,
424+
operator: 'equals',
425+
severity: 'critical',
426+
},
427+
];
428+
const doc = sampleDocSeverity(Number(eventSeverity));
429+
const rule = buildRuleWithOverrides(ruleSO, doc._source);
430+
const expected = {
431+
...expectedRule(),
432+
severity: 'critical',
433+
severity_mapping: ruleSO.attributes.params.severityMapping,
434+
meta: {
435+
severityOverrideField: 'event.severity',
436+
},
437+
};
438+
expect(rule).toEqual(expected);
400439
});
401440
});

0 commit comments

Comments
 (0)