[Security Solution] Fixes threat field appearing as modified when reset to base version value (#208530)

**Fixes https://github.com/elastic/kibana/issues/208251**

## Summary

This bug was caused by the local generated MITRE data we have stored in
`x-pack/solutions/security/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts`
having an inconsistency in the way its reference urls were written
compared to the TRADE team's prebuilt rule packages. The trailing
backslash was present in the prebuilt rule packages (and added by
browsers) but not in the url field from the `.json` file we scrape the
MITRE data from in our script.

For example, this is the url from the script: 

```
https://attack.mitre.org/techniques/T1078/004
```

and this is the url directly from the rule package:

```
https://attack.mitre.org/techniques/T1078/004/
```

This PR adds a normalization function that adds a trailing backslash to
the comparison string for the diff algorithm if it doesn't already
exist.

### 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
This commit is contained in:
Davis Plumlee 2025-02-05 02:32:42 +07:00 committed by GitHub
parent 5a18f66ed6
commit 6f55501a75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 803 additions and 671 deletions

View file

@ -12,6 +12,87 @@ import { extractThreatArray } from './extract_threat_array';
const mockThreat = getThreatMock()[0];
describe('extractThreatArray', () => {
it('normalizes url ending backslashes', () => {
const mockRule = {
...getRulesSchemaMock(),
threat: [
{
...mockThreat,
tactic: {
...mockThreat.tactic,
reference: 'https://attack.mitre.org/tactics/TA0000',
},
technique: [
{
...mockThreat.technique![0],
reference: 'https://attack.mitre.org/techniques/T0000/',
subtechnique: [
{
...mockThreat.technique![0].subtechnique![0],
reference: 'https://attack.mitre.org/techniques/T0000/000',
},
],
},
],
},
],
};
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/',
subtechnique: [
{
id: 'T0000.000',
name: 'test subtechnique',
reference: 'https://attack.mitre.org/techniques/T0000/000/',
},
],
},
],
},
]);
});
it('normalizes url ending backslashes with query strings', () => {
const mockRule = {
...getRulesSchemaMock(),
threat: [
{
...mockThreat,
tactic: {
...mockThreat.tactic,
reference: 'https://attack.mitre.org/tactics/TA0000?query=test',
},
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/?query=test',
},
},
]);
});
it('trims empty technique fields from threat object', () => {
const mockRule = { ...getRulesSchemaMock(), threat: [{ ...mockThreat, technique: [] }] };
const normalizedThreatArray = extractThreatArray(mockRule);

View file

@ -8,21 +8,56 @@
import type {
RuleResponse,
ThreatArray,
ThreatSubtechnique,
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,
tactic: { ...threat.tactic, reference: normalizeThreatReference(threat.tactic.reference) },
technique: trimTechniqueArray(threat.technique),
};
}
return { ...threat, technique: undefined }; // If `technique` is an empty array, remove the field from the `threat` object
return {
...threat,
tactic: { ...threat.tactic, reference: normalizeThreatReference(threat.tactic.reference) },
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,
reference: normalizeThreatReference(technique.reference),
subtechnique:
technique.subtechnique && technique.subtechnique.length ? technique.subtechnique : undefined, // If `subtechnique` is an empty array, remove the field from the `technique` object
technique.subtechnique && technique.subtechnique.length
? trimSubtechniqueArray(technique.subtechnique)
: undefined, // If `subtechnique` is an empty array, remove the field from the `technique` object
}));
};
const trimSubtechniqueArray = (subtechniqueArray: ThreatSubtechnique[]): ThreatSubtechnique[] => {
return subtechniqueArray.map((subtechnique) => ({
...subtechnique,
reference: normalizeThreatReference(subtechnique.reference),
}));
};
const normalizeThreatReference = (reference: string): string => {
try {
const parsed = new URL(reference);
if (!parsed.pathname.endsWith('/')) {
// Adds a trailing backslash in urls if it doesn't exist to account for
// any inconsistencies between our script generated data and prebuilt rules packages
parsed.pathname = `${parsed.pathname}/`;
}
return parsed.toString();
} catch {
return reference;
}
};

View file

@ -80,12 +80,28 @@ const getSubtechniquesOptions = (subtechniques) =>
}`.replace(/(\r\n|\n|\r)/gm, ' ')
);
const normalizeThreatReference = (reference) => {
try {
const parsed = new URL(reference);
if (!parsed.pathname.endsWith('/')) {
// Adds a trailing backslash in urls if it doesn't exist to account for
// any inconsistencies between our script generated data and prebuilt rules packages
parsed.pathname = `${parsed.pathname}/`;
}
return parsed.toString();
} catch {
return reference;
}
};
const getIdReference = (references) => {
const ref = references.find((r) => r.source_name === 'mitre-attack');
if (ref != null) {
return {
id: ref.external_id,
reference: ref.url,
reference: normalizeThreatReference(ref.url),
};
} else {
return { id: '', reference: '' };