Skip to content

Commit 6df8d65

Browse files
authored
[Security Solution][Detection Rules] Update prebuilt rule threats to match schema (#92281) (#92721)
1 parent 044de1e commit 6df8d65

File tree

10 files changed

+137
-98
lines changed

10 files changed

+137
-98
lines changed

x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -444,13 +444,19 @@ export const threat_technique = t.intersection([
444444
]);
445445
export type ThreatTechnique = t.TypeOf<typeof threat_technique>;
446446
export const threat_techniques = t.array(threat_technique);
447-
export const threat = t.exact(
448-
t.type({
449-
framework: threat_framework,
450-
tactic: threat_tactic,
451-
technique: threat_techniques,
452-
})
453-
);
447+
export const threat = t.intersection([
448+
t.exact(
449+
t.type({
450+
framework: threat_framework,
451+
tactic: threat_tactic,
452+
})
453+
),
454+
t.exact(
455+
t.partial({
456+
technique: threat_techniques,
457+
})
458+
),
459+
]);
454460
export type Threat = t.TypeOf<typeof threat>;
455461

456462
export const threats = t.array(threat);

x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ describe('add prepackaged rules schema', () => {
924924
expect(message.schema).toEqual({});
925925
});
926926

927-
test('You cannot send in an array of threat that are missing "technique"', () => {
927+
test('You can send in an array of threat that are missing "technique"', () => {
928928
const payload: Omit<AddPrepackagedRulesSchema, 'threat'> & {
929929
threat: Array<Partial<Omit<AddPrepackagedRulesSchema['threat'], 'technique'>>>;
930930
} = {
@@ -944,10 +944,21 @@ describe('add prepackaged rules schema', () => {
944944
const decoded = addPrepackagedRulesSchema.decode(payload);
945945
const checked = exactCheck(payload, decoded);
946946
const message = pipe(checked, foldLeftRight);
947-
expect(getPaths(left(message.errors))).toEqual([
948-
'Invalid value "undefined" supplied to "threat,technique"',
949-
]);
950-
expect(message.schema).toEqual({});
947+
expect(getPaths(left(message.errors))).toEqual([]);
948+
const expected: AddPrepackagedRulesSchemaDecoded = {
949+
...getAddPrepackagedRulesSchemaDecodedMock(),
950+
threat: [
951+
{
952+
framework: 'fake',
953+
tactic: {
954+
id: 'fakeId',
955+
name: 'fakeName',
956+
reference: 'fakeRef',
957+
},
958+
},
959+
],
960+
};
961+
expect(message.schema).toEqual(expected);
951962
});
952963

953964
test('You can optionally send in an array of false positives', () => {

x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ describe('import rules schema', () => {
926926
expect(message.schema).toEqual({});
927927
});
928928

929-
test('You cannot send in an array of threat that are missing "technique"', () => {
929+
test('You can send in an array of threat that are missing "technique"', () => {
930930
const payload: Omit<ImportRulesSchema, 'threat'> & {
931931
threat: Array<Partial<Omit<ImportRulesSchema['threat'], 'technique'>>>;
932932
} = {
@@ -946,10 +946,21 @@ describe('import rules schema', () => {
946946
const decoded = importRulesSchema.decode(payload);
947947
const checked = exactCheck(payload, decoded);
948948
const message = pipe(checked, foldLeftRight);
949-
expect(getPaths(left(message.errors))).toEqual([
950-
'Invalid value "undefined" supplied to "threat,technique"',
951-
]);
952-
expect(message.schema).toEqual({});
949+
expect(getPaths(left(message.errors))).toEqual([]);
950+
const expected: ImportRulesSchemaDecoded = {
951+
...getImportRulesSchemaDecodedMock(),
952+
threat: [
953+
{
954+
framework: 'fake',
955+
tactic: {
956+
id: 'fakeId',
957+
name: 'fakeName',
958+
reference: 'fakeRef',
959+
},
960+
},
961+
],
962+
};
963+
expect(message.schema).toEqual(expected);
953964
});
954965

955966
test('You can optionally send in an array of false positives', () => {

x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ describe('patch_rules_schema', () => {
973973
expect(message.schema).toEqual({});
974974
});
975975

976-
test('threat is invalid when updated with missing technique', () => {
976+
test('threat is valid when updated with missing technique', () => {
977977
const threat: Omit<PatchRulesSchema['threat'], 'technique'> = [
978978
{
979979
framework: 'fake',
@@ -993,10 +993,8 @@ describe('patch_rules_schema', () => {
993993
const decoded = patchRulesSchema.decode(payload);
994994
const checked = exactCheck(payload, decoded);
995995
const message = pipe(checked, foldLeftRight);
996-
expect(getPaths(left(message.errors))).toEqual([
997-
'Invalid value "undefined" supplied to "threat,technique"',
998-
]);
999-
expect(message.schema).toEqual({});
996+
expect(getPaths(left(message.errors))).toEqual([]);
997+
expect(message.schema).toEqual(payload);
1000998
});
1001999

10021000
test('validates with timeline_id and timeline_title', () => {

x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ describe('create rules schema', () => {
618618
expect(message.schema).toEqual({});
619619
});
620620

621-
test('You cannot send in an array of threat that are missing "technique"', () => {
621+
test('You can send in an array of threat that are missing "technique"', () => {
622622
const payload = {
623623
...getCreateRulesSchemaMock(),
624624
threat: [
@@ -636,10 +636,8 @@ describe('create rules schema', () => {
636636
const decoded = createRulesSchema.decode(payload);
637637
const checked = exactCheck(payload, decoded);
638638
const message = pipe(checked, foldLeftRight);
639-
expect(getPaths(left(message.errors))).toEqual([
640-
'Invalid value "undefined" supplied to "threat,technique"',
641-
]);
642-
expect(message.schema).toEqual({});
639+
expect(getPaths(left(message.errors))).toEqual([]);
640+
expect(message.schema).toEqual(payload);
643641
});
644642

645643
test('You can optionally send in an array of false positives', () => {

x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -157,49 +157,54 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription
157157
: `${singleThreat.tactic.name} (${singleThreat.tactic.id})`}
158158
</EuiLink>
159159
<EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column">
160-
{singleThreat.technique.map((technique, techniqueIndex) => {
161-
const myTechnique = techniquesOptions.find((t) => t.id === technique.id);
162-
return (
163-
<EuiFlexItem key={myTechnique?.id ?? techniqueIndex}>
164-
<TechniqueLinkItem
165-
data-test-subj="threatTechniqueLink"
166-
href={technique.reference}
167-
target="_blank"
168-
iconType={ListTreeIcon}
169-
size="xs"
170-
>
171-
{myTechnique != null
172-
? myTechnique.label
173-
: `${technique.name} (${technique.id})`}
174-
</TechniqueLinkItem>
175-
<EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column">
176-
{technique.subtechnique != null &&
177-
technique.subtechnique.map((subtechnique, subtechniqueIndex) => {
178-
const mySubtechnique = subtechniquesOptions.find(
179-
(t) => t.id === subtechnique.id
180-
);
181-
return (
182-
<SubtechniqueFlexItem
183-
key={mySubtechnique?.id ?? subtechniqueIndex}
184-
>
185-
<TechniqueLinkItem
186-
data-test-subj="threatSubtechniqueLink"
187-
href={subtechnique.reference}
188-
target="_blank"
189-
iconType={ListTreeIcon}
190-
size="xs"
160+
{singleThreat.technique &&
161+
singleThreat.technique.map((technique, techniqueIndex) => {
162+
const myTechnique = techniquesOptions.find((t) => t.id === technique.id);
163+
return (
164+
<EuiFlexItem key={myTechnique?.id ?? techniqueIndex}>
165+
<TechniqueLinkItem
166+
data-test-subj="threatTechniqueLink"
167+
href={technique.reference}
168+
target="_blank"
169+
iconType={ListTreeIcon}
170+
size="xs"
171+
>
172+
{myTechnique != null
173+
? myTechnique.label
174+
: `${technique.name} (${technique.id})`}
175+
</TechniqueLinkItem>
176+
<EuiFlexGroup
177+
gutterSize="none"
178+
alignItems="flexStart"
179+
direction="column"
180+
>
181+
{technique.subtechnique != null &&
182+
technique.subtechnique.map((subtechnique, subtechniqueIndex) => {
183+
const mySubtechnique = subtechniquesOptions.find(
184+
(t) => t.id === subtechnique.id
185+
);
186+
return (
187+
<SubtechniqueFlexItem
188+
key={mySubtechnique?.id ?? subtechniqueIndex}
191189
>
192-
{mySubtechnique != null
193-
? mySubtechnique.label
194-
: `${subtechnique.name} (${subtechnique.id})`}
195-
</TechniqueLinkItem>
196-
</SubtechniqueFlexItem>
197-
);
198-
})}
199-
</EuiFlexGroup>
200-
</EuiFlexItem>
201-
);
202-
})}
190+
<TechniqueLinkItem
191+
data-test-subj="threatSubtechniqueLink"
192+
href={subtechnique.reference}
193+
target="_blank"
194+
iconType={ListTreeIcon}
195+
size="xs"
196+
>
197+
{mySubtechnique != null
198+
? mySubtechnique.label
199+
: `${subtechnique.name} (${subtechnique.id})`}
200+
</TechniqueLinkItem>
201+
</SubtechniqueFlexItem>
202+
);
203+
})}
204+
</EuiFlexGroup>
205+
</EuiFlexItem>
206+
);
207+
})}
203208
</EuiFlexGroup>
204209
</EuiFlexItem>
205210
);

x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { getValidThreat } from '../../../mitre/valid_threat_mock';
99
import { hasSubtechniqueOptions } from './helpers';
1010

11-
const mockTechniques = getValidThreat()[0].technique;
11+
const mockTechniques = getValidThreat()[0].technique ?? [];
1212

1313
describe('helpers', () => {
1414
describe('hasSubtechniqueOptions', () => {

x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,45 +51,46 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
5151
const values = field.value as Threats;
5252

5353
const technique = useMemo(() => {
54-
return values[threatIndex].technique[techniqueIndex];
55-
}, [values, threatIndex, techniqueIndex]);
54+
return [...(values[threatIndex].technique ?? [])];
55+
}, [values, threatIndex]);
5656

5757
const removeSubtechnique = useCallback(
5858
(index: number) => {
5959
const threats = [...(field.value as Threats)];
60-
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
60+
const subtechniques = technique[techniqueIndex].subtechnique ?? [];
6161
if (subtechniques != null) {
6262
subtechniques.splice(index, 1);
6363

64-
threats[threatIndex].technique[techniqueIndex] = {
65-
...threats[threatIndex].technique[techniqueIndex],
64+
technique[techniqueIndex] = {
65+
...technique[techniqueIndex],
6666
subtechnique: subtechniques,
6767
};
68+
threats[threatIndex].technique = technique;
6869
onFieldChange(threats);
6970
}
7071
},
71-
[field, threatIndex, onFieldChange, techniqueIndex]
72+
[field, onFieldChange, techniqueIndex, technique, threatIndex]
7273
);
7374

7475
const addMitreAttackSubtechnique = useCallback(() => {
7576
const threats = [...(field.value as Threats)];
7677

77-
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
78+
const subtechniques = technique[techniqueIndex].subtechnique;
7879

7980
if (subtechniques != null) {
80-
threats[threatIndex].technique[techniqueIndex] = {
81-
...threats[threatIndex].technique[techniqueIndex],
81+
technique[techniqueIndex] = {
82+
...technique[techniqueIndex],
8283
subtechnique: [...subtechniques, { id: 'none', name: 'none', reference: 'none' }],
8384
};
8485
} else {
85-
threats[threatIndex].technique[techniqueIndex] = {
86-
...threats[threatIndex].technique[techniqueIndex],
86+
technique[techniqueIndex] = {
87+
...technique[techniqueIndex],
8788
subtechnique: [{ id: 'none', name: 'none', reference: 'none' }],
8889
};
8990
}
90-
91+
threats[threatIndex].technique = technique;
9192
onFieldChange(threats);
92-
}, [field, threatIndex, onFieldChange, techniqueIndex]);
93+
}, [field, onFieldChange, techniqueIndex, technique, threatIndex]);
9394

9495
const updateSubtechnique = useCallback(
9596
(index: number, value: string) => {
@@ -99,17 +100,17 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
99100
name: '',
100101
reference: '',
101102
};
102-
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
103+
const subtechniques = technique[techniqueIndex].subtechnique;
103104

104105
if (subtechniques != null) {
105106
onFieldChange([
106107
...threats.slice(0, threatIndex),
107108
{
108109
...threats[threatIndex],
109110
technique: [
110-
...threats[threatIndex].technique.slice(0, techniqueIndex),
111+
...technique.slice(0, techniqueIndex),
111112
{
112-
...threats[threatIndex].technique[techniqueIndex],
113+
...technique[techniqueIndex],
113114
subtechnique: [
114115
...subtechniques.slice(0, index),
115116
{
@@ -120,19 +121,21 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
120121
...subtechniques.slice(index + 1),
121122
],
122123
},
123-
...threats[threatIndex].technique.slice(techniqueIndex + 1),
124+
...technique.slice(techniqueIndex + 1),
124125
],
125126
},
126127
...threats.slice(threatIndex + 1),
127128
]);
128129
}
129130
},
130-
[threatIndex, techniqueIndex, onFieldChange, field]
131+
[threatIndex, techniqueIndex, onFieldChange, field, technique]
131132
);
132133

133134
const getSelectSubtechnique = useCallback(
134135
(index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => {
135-
const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id);
136+
const options = subtechniquesOptions.filter(
137+
(t) => t.techniqueId === technique[techniqueIndex].id
138+
);
136139

137140
return (
138141
<>
@@ -166,13 +169,17 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
166169
</>
167170
);
168171
},
169-
[field, updateSubtechnique, technique]
172+
[field, updateSubtechnique, technique, techniqueIndex]
170173
);
171174

175+
const subtechniques = useMemo(() => {
176+
return technique[techniqueIndex].subtechnique;
177+
}, [technique, techniqueIndex]);
178+
172179
return (
173180
<SubtechniqueContainer>
174-
{technique.subtechnique != null &&
175-
technique.subtechnique.map((subtechnique, index) => (
181+
{subtechniques != null &&
182+
subtechniques.map((subtechnique, index) => (
176183
<div key={index}>
177184
<EuiSpacer size="s" />
178185
<EuiFormRow

0 commit comments

Comments
 (0)