[8.16] [Security Solution] Fixes data normalization in diff algorithms for `threat` and `rule_schedule` fields (#200105) (#200645)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Security Solution] Fixes data normalization in diff algorithms for
`threat` and `rule_schedule` fields
(#200105)](https://github.com/elastic/kibana/pull/200105)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Davis
Plumlee","email":"56367316+dplumlee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-11-18T19:48:14Z","message":"[Security
Solution] Fixes data normalization in diff algorithms for `threat` and
`rule_schedule` fields (#200105)\n\n**Fixes
https://github.com/elastic/kibana/issues/199629**\r\n\r\n##
Summary\r\n\r\nFixes the data normalization we do before comparison for
the `threat`\r\nand `rule_schedule` fields so that they align with our
prebuilt rule\r\nspecs. Specifically:\r\n\r\n- Trims any extra optional
nested fields in the `threat` field that were\r\nleft as empty
arrays\r\n- Removes the logic to use the `from` value in the `meta`
field if it\r\nexisted, so that we can normalize the time strings for
`rule_schedule`\r\n\r\nThese errors were occurring when a rule was saved
via the Rule Editing\r\nform in the UI and extra fields were added in
the update API call. This\r\nPR makes the diff algorithms more robust
against different field values\r\nthat are represented differently but
are logically the same.\r\n\r\nThis extra data added in the Rule Edit UI
form was also causing rules to\r\nappear as modified when saved from the
form, even if no fields had been\r\nmodified.\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests
changed","sha":"a8fd0c95148ab42411e5ad8e6a65df0634f67dbe","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","impact:high","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:version","v8.17.0","v8.16.1"],"title":"[Security
Solution] Fixes data normalization in diff algorithms for `threat` and
`rule_schedule`
fields","number":200105,"url":"https://github.com/elastic/kibana/pull/200105","mergeCommit":{"message":"[Security
Solution] Fixes data normalization in diff algorithms for `threat` and
`rule_schedule` fields (#200105)\n\n**Fixes
https://github.com/elastic/kibana/issues/199629**\r\n\r\n##
Summary\r\n\r\nFixes the data normalization we do before comparison for
the `threat`\r\nand `rule_schedule` fields so that they align with our
prebuilt rule\r\nspecs. Specifically:\r\n\r\n- Trims any extra optional
nested fields in the `threat` field that were\r\nleft as empty
arrays\r\n- Removes the logic to use the `from` value in the `meta`
field if it\r\nexisted, so that we can normalize the time strings for
`rule_schedule`\r\n\r\nThese errors were occurring when a rule was saved
via the Rule Editing\r\nform in the UI and extra fields were added in
the update API call. This\r\nPR makes the diff algorithms more robust
against different field values\r\nthat are represented differently but
are logically the same.\r\n\r\nThis extra data added in the Rule Edit UI
form was also causing rules to\r\nappear as modified when saved from the
form, even if no fields had been\r\nmodified.\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests
changed","sha":"a8fd0c95148ab42411e5ad8e6a65df0634f67dbe"}},"sourceBranch":"main","suggestedTargetBranches":["8.x","8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/200105","number":200105,"mergeCommit":{"message":"[Security
Solution] Fixes data normalization in diff algorithms for `threat` and
`rule_schedule` fields (#200105)\n\n**Fixes
https://github.com/elastic/kibana/issues/199629**\r\n\r\n##
Summary\r\n\r\nFixes the data normalization we do before comparison for
the `threat`\r\nand `rule_schedule` fields so that they align with our
prebuilt rule\r\nspecs. Specifically:\r\n\r\n- Trims any extra optional
nested fields in the `threat` field that were\r\nleft as empty
arrays\r\n- Removes the logic to use the `from` value in the `meta`
field if it\r\nexisted, so that we can normalize the time strings for
`rule_schedule`\r\n\r\nThese errors were occurring when a rule was saved
via the Rule Editing\r\nform in the UI and extra fields were added in
the update API call. This\r\nPR makes the diff algorithms more robust
against different field values\r\nthat are represented differently but
are logically the same.\r\n\r\nThis extra data added in the Rule Edit UI
form was also causing rules to\r\nappear as modified when saved from the
form, even if no fields had been\r\nmodified.\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests
changed","sha":"a8fd0c95148ab42411e5ad8e6a65df0634f67dbe"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.16","label":"v8.16.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-11-19 08:36:29 +11:00 committed by GitHub
parent b2c5123e03
commit c8b46e87c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 108 additions and 20 deletions

View file

@ -53,6 +53,7 @@ import { extractRuleNameOverrideObject } from './extract_rule_name_override_obje
import { extractRuleSchedule } from './extract_rule_schedule';
import { extractTimelineTemplateReference } from './extract_timeline_template_reference';
import { extractTimestampOverrideObject } from './extract_timestamp_override_object';
import { extractThreatArray } from './extract_threat_array';
/**
* Normalizes a given rule to the form which is suitable for passing to the diff algorithm.
@ -128,7 +129,7 @@ const extractDiffableCommonFields = (
// About -> Advanced settings
references: rule.references ?? [],
false_positives: rule.false_positives ?? [],
threat: rule.threat ?? [],
threat: extractThreatArray(rule),
note: rule.note ?? '',
setup: rule.setup ?? '',
related_integrations: rule.related_integrations ?? [],

View file

@ -0,0 +1,18 @@
/*
* 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 { getRulesSchemaMock } from '../../../api/detection_engine/model/rule_schema/mocks';
import { extractRuleSchedule } from './extract_rule_schedule';
describe('extractRuleSchedule', () => {
it('normalizes lookback strings to seconds', () => {
const mockRule = { ...getRulesSchemaMock(), from: 'now-6m', interval: '5m', to: 'now' };
const normalizedRuleSchedule = extractRuleSchedule(mockRule);
expect(normalizedRuleSchedule).toEqual({ interval: '5m', lookback: '60s' });
});
});

View file

@ -9,7 +9,7 @@ import moment from 'moment';
import dateMath from '@elastic/datemath';
import { parseDuration } from '@kbn/alerting-plugin/common';
import type { RuleMetadata, RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { RuleSchedule } from '../../../api/detection_engine/prebuilt_rules';
export const extractRuleSchedule = (rule: RuleResponse): RuleSchedule => {
@ -17,26 +17,9 @@ export const extractRuleSchedule = (rule: RuleResponse): RuleSchedule => {
const from = rule.from ?? 'now-6m';
const to = rule.to ?? 'now';
const ruleMeta: RuleMetadata = ('meta' in rule ? rule.meta : undefined) ?? {};
const lookbackFromMeta = String(ruleMeta.from ?? '');
const intervalDuration = parseInterval(interval);
const lookbackFromMetaDuration = parseInterval(lookbackFromMeta);
const driftToleranceDuration = parseDriftTolerance(from, to);
if (lookbackFromMetaDuration != null) {
if (intervalDuration != null) {
return {
interval,
lookback: lookbackFromMeta,
};
}
return {
interval: `Cannot parse: interval="${interval}"`,
lookback: lookbackFromMeta,
};
}
if (intervalDuration == null) {
return {
interval: `Cannot parse: interval="${interval}"`,

View file

@ -0,0 +1,56 @@
/*
* 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 { getRulesSchemaMock } from '../../../api/detection_engine/model/rule_schema/mocks';
import { getThreatMock } from '../../schemas/types/threat.mock';
import { extractThreatArray } from './extract_threat_array';
const mockThreat = getThreatMock()[0];
describe('extractThreatArray', () => {
it('trims empty technique fields from threat object', () => {
const mockRule = { ...getRulesSchemaMock(), threat: [{ ...mockThreat, technique: [] }] };
const normalizedThreatArray = extractThreatArray(mockRule);
expect(normalizedThreatArray).toEqual([
{
framework: 'MITRE ATT&CK',
tactic: {
id: 'TA0000',
name: 'test tactic',
reference: 'https://attack.mitre.org/tactics/TA0000/',
},
},
]);
});
it('trims empty subtechnique fields from threat object', () => {
const mockRule = {
...getRulesSchemaMock(),
threat: [{ ...mockThreat, technique: [{ ...mockThreat.technique![0], subtechnique: [] }] }],
};
const normalizedThreatArray = extractThreatArray(mockRule);
expect(normalizedThreatArray).toEqual([
{
framework: 'MITRE ATT&CK',
tactic: {
id: 'TA0000',
name: 'test tactic',
reference: 'https://attack.mitre.org/tactics/TA0000/',
},
technique: [
{
id: 'T0000',
name: 'test technique',
reference: 'https://attack.mitre.org/techniques/T0000/',
},
],
},
]);
});
});

View file

@ -0,0 +1,28 @@
/*
* 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 type {
RuleResponse,
ThreatArray,
ThreatTechnique,
} from '../../../api/detection_engine/model/rule_schema';
export const extractThreatArray = (rule: RuleResponse): ThreatArray =>
rule.threat.map((threat) => {
if (threat.technique && threat.technique.length) {
return { ...threat, technique: trimTechniqueArray(threat.technique) };
}
return { ...threat, technique: undefined }; // If `technique` is an empty array, remove the field from the `threat` object
});
const trimTechniqueArray = (techniqueArray: ThreatTechnique[]): ThreatTechnique[] => {
return techniqueArray.map((technique) => ({
...technique,
subtechnique:
technique.subtechnique && technique.subtechnique.length ? technique.subtechnique : undefined, // If `subtechnique` is an empty array, remove the field from the `technique` object
}));
};

View file

@ -375,7 +375,9 @@ export const filterEmptyThreats = (threats: Threats): Threats => {
return {
...technique,
subtechnique:
technique.subtechnique != null ? trimThreatsWithNoName(technique.subtechnique) : [],
technique.subtechnique != null
? trimThreatsWithNoName(technique.subtechnique)
: undefined,
};
}),
};