Skip to content

Commit

Permalink
[8.x] [Security Solution] Fixes data normalization in diff algorithms…
Browse files Browse the repository at this point in the history
… for `threat` and `rule_schedule` fields (#200105) (#200646)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Fixes data normalization in diff algorithms for
`threat` and `rule_schedule` fields
(#200105)](#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":"[email protected]"},"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 <[email protected]>
  • Loading branch information
kibanamachine and dplumlee authored Nov 18, 2024
1 parent f41ddf0 commit 29e3669
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 ?? [],
Expand Down
Original file line number Diff line number Diff line change
@@ -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' });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,17 @@ 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 => {
const interval = rule.interval ?? '5m';
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}"`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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/',
},
],
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -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
}));
};
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,9 @@ export const filterEmptyThreats = (threats: Threats): Threats => {
return {
...technique,
subtechnique:
technique.subtechnique != null ? trimThreatsWithNoName(technique.subtechnique) : [],
technique.subtechnique != null
? trimThreatsWithNoName(technique.subtechnique)
: undefined,
};
}),
};
Expand Down

0 comments on commit 29e3669

Please sign in to comment.