Skip to content

Commit 7e214ac

Browse files
committed
[Security Solution] Fixes data normalization in diff algorithms for threat and rule_schedule fields (#200105)
**Fixes #199629 ## Summary Fixes the data normalization we do before comparison for the `threat` and `rule_schedule` fields so that they align with our prebuilt rule specs. Specifically: - Trims any extra optional nested fields in the `threat` field that were left as empty arrays - Removes the logic to use the `from` value in the `meta` field if it existed, so that we can normalize the time strings for `rule_schedule` These errors were occurring when a rule was saved via the Rule Editing form in the UI and extra fields were added in the update API call. This PR makes the diff algorithms more robust against different field values that are represented differently but are logically the same. This extra data added in the Rule Edit UI form was also causing rules to appear as modified when saved from the form, even if no fields had been modified. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed (cherry picked from commit a8fd0c9)
1 parent 0699913 commit 7e214ac

6 files changed

Lines changed: 108 additions & 20 deletions

File tree

x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { extractRuleNameOverrideObject } from './extract_rule_name_override_obje
5353
import { extractRuleSchedule } from './extract_rule_schedule';
5454
import { extractTimelineTemplateReference } from './extract_timeline_template_reference';
5555
import { extractTimestampOverrideObject } from './extract_timestamp_override_object';
56+
import { extractThreatArray } from './extract_threat_array';
5657

5758
/**
5859
* Normalizes a given rule to the form which is suitable for passing to the diff algorithm.
@@ -128,7 +129,7 @@ const extractDiffableCommonFields = (
128129
// About -> Advanced settings
129130
references: rule.references ?? [],
130131
false_positives: rule.false_positives ?? [],
131-
threat: rule.threat ?? [],
132+
threat: extractThreatArray(rule),
132133
note: rule.note ?? '',
133134
setup: rule.setup ?? '',
134135
related_integrations: rule.related_integrations ?? [],
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { getRulesSchemaMock } from '../../../api/detection_engine/model/rule_schema/mocks';
9+
import { extractRuleSchedule } from './extract_rule_schedule';
10+
11+
describe('extractRuleSchedule', () => {
12+
it('normalizes lookback strings to seconds', () => {
13+
const mockRule = { ...getRulesSchemaMock(), from: 'now-6m', interval: '5m', to: 'now' };
14+
const normalizedRuleSchedule = extractRuleSchedule(mockRule);
15+
16+
expect(normalizedRuleSchedule).toEqual({ interval: '5m', lookback: '60s' });
17+
});
18+
});

x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_schedule.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,17 @@ import moment from 'moment';
99
import dateMath from '@elastic/datemath';
1010
import { parseDuration } from '@kbn/alerting-plugin/common';
1111

12-
import type { RuleMetadata, RuleResponse } from '../../../api/detection_engine/model/rule_schema';
12+
import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema';
1313
import type { RuleSchedule } from '../../../api/detection_engine/prebuilt_rules';
1414

1515
export const extractRuleSchedule = (rule: RuleResponse): RuleSchedule => {
1616
const interval = rule.interval ?? '5m';
1717
const from = rule.from ?? 'now-6m';
1818
const to = rule.to ?? 'now';
1919

20-
const ruleMeta: RuleMetadata = ('meta' in rule ? rule.meta : undefined) ?? {};
21-
const lookbackFromMeta = String(ruleMeta.from ?? '');
22-
2320
const intervalDuration = parseInterval(interval);
24-
const lookbackFromMetaDuration = parseInterval(lookbackFromMeta);
2521
const driftToleranceDuration = parseDriftTolerance(from, to);
2622

27-
if (lookbackFromMetaDuration != null) {
28-
if (intervalDuration != null) {
29-
return {
30-
interval,
31-
lookback: lookbackFromMeta,
32-
};
33-
}
34-
return {
35-
interval: `Cannot parse: interval="${interval}"`,
36-
lookback: lookbackFromMeta,
37-
};
38-
}
39-
4023
if (intervalDuration == null) {
4124
return {
4225
interval: `Cannot parse: interval="${interval}"`,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { getRulesSchemaMock } from '../../../api/detection_engine/model/rule_schema/mocks';
9+
import { getThreatMock } from '../../schemas/types/threat.mock';
10+
import { extractThreatArray } from './extract_threat_array';
11+
12+
const mockThreat = getThreatMock()[0];
13+
14+
describe('extractThreatArray', () => {
15+
it('trims empty technique fields from threat object', () => {
16+
const mockRule = { ...getRulesSchemaMock(), threat: [{ ...mockThreat, technique: [] }] };
17+
const normalizedThreatArray = extractThreatArray(mockRule);
18+
19+
expect(normalizedThreatArray).toEqual([
20+
{
21+
framework: 'MITRE ATT&CK',
22+
tactic: {
23+
id: 'TA0000',
24+
name: 'test tactic',
25+
reference: 'https://attack.mitre.org/tactics/TA0000/',
26+
},
27+
},
28+
]);
29+
});
30+
31+
it('trims empty subtechnique fields from threat object', () => {
32+
const mockRule = {
33+
...getRulesSchemaMock(),
34+
threat: [{ ...mockThreat, technique: [{ ...mockThreat.technique![0], subtechnique: [] }] }],
35+
};
36+
const normalizedThreatArray = extractThreatArray(mockRule);
37+
38+
expect(normalizedThreatArray).toEqual([
39+
{
40+
framework: 'MITRE ATT&CK',
41+
tactic: {
42+
id: 'TA0000',
43+
name: 'test tactic',
44+
reference: 'https://attack.mitre.org/tactics/TA0000/',
45+
},
46+
technique: [
47+
{
48+
id: 'T0000',
49+
name: 'test technique',
50+
reference: 'https://attack.mitre.org/techniques/T0000/',
51+
},
52+
],
53+
},
54+
]);
55+
});
56+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type {
9+
RuleResponse,
10+
ThreatArray,
11+
ThreatTechnique,
12+
} from '../../../api/detection_engine/model/rule_schema';
13+
14+
export const extractThreatArray = (rule: RuleResponse): ThreatArray =>
15+
rule.threat.map((threat) => {
16+
if (threat.technique && threat.technique.length) {
17+
return { ...threat, technique: trimTechniqueArray(threat.technique) };
18+
}
19+
return { ...threat, technique: undefined }; // If `technique` is an empty array, remove the field from the `threat` object
20+
});
21+
22+
const trimTechniqueArray = (techniqueArray: ThreatTechnique[]): ThreatTechnique[] => {
23+
return techniqueArray.map((technique) => ({
24+
...technique,
25+
subtechnique:
26+
technique.subtechnique && technique.subtechnique.length ? technique.subtechnique : undefined, // If `subtechnique` is an empty array, remove the field from the `technique` object
27+
}));
28+
};

x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,9 @@ export const filterEmptyThreats = (threats: Threats): Threats => {
375375
return {
376376
...technique,
377377
subtechnique:
378-
technique.subtechnique != null ? trimThreatsWithNoName(technique.subtechnique) : [],
378+
technique.subtechnique != null
379+
? trimThreatsWithNoName(technique.subtechnique)
380+
: undefined,
379381
};
380382
}),
381383
};

0 commit comments

Comments
 (0)