diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index d97820f010a80..bfe450d240b08 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -444,13 +444,19 @@ export const threat_technique = t.intersection([ ]); export type ThreatTechnique = t.TypeOf; export const threat_techniques = t.array(threat_technique); -export const threat = t.exact( - t.type({ - framework: threat_framework, - tactic: threat_tactic, - technique: threat_techniques, - }) -); +export const threat = t.intersection([ + t.exact( + t.type({ + framework: threat_framework, + tactic: threat_tactic, + }) + ), + t.exact( + t.partial({ + technique: threat_techniques, + }) + ), +]); export type Threat = t.TypeOf; export const threats = t.array(threat); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index 93094e3445488..f3bef5ad7445f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -924,7 +924,7 @@ describe('add prepackaged rules schema', () => { expect(message.schema).toEqual({}); }); - test('You cannot send in an array of threat that are missing "technique"', () => { + test('You can send in an array of threat that are missing "technique"', () => { const payload: Omit & { threat: Array>>; } = { @@ -944,10 +944,21 @@ describe('add prepackaged rules schema', () => { const decoded = addPrepackagedRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + expect(message.schema).toEqual(expected); }); test('You can optionally send in an array of false positives', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index a59c873658411..2caedd2e01193 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -926,7 +926,7 @@ describe('import rules schema', () => { expect(message.schema).toEqual({}); }); - test('You cannot send in an array of threat that are missing "technique"', () => { + test('You can send in an array of threat that are missing "technique"', () => { const payload: Omit & { threat: Array>>; } = { @@ -946,10 +946,21 @@ describe('import rules schema', () => { const decoded = importRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + expect(message.schema).toEqual(expected); }); test('You can optionally send in an array of false positives', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts index 8cdb85a555451..3dfa12acc29d5 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts @@ -973,7 +973,7 @@ describe('patch_rules_schema', () => { expect(message.schema).toEqual({}); }); - test('threat is invalid when updated with missing technique', () => { + test('threat is valid when updated with missing technique', () => { const threat: Omit = [ { framework: 'fake', @@ -993,10 +993,8 @@ describe('patch_rules_schema', () => { const decoded = patchRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); }); test('validates with timeline_id and timeline_title', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 6b8211b23088c..70ff921d3b334 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -618,7 +618,7 @@ describe('create rules schema', () => { expect(message.schema).toEqual({}); }); - test('You cannot send in an array of threat that are missing "technique"', () => { + test('You can send in an array of threat that are missing "technique"', () => { const payload = { ...getCreateRulesSchemaMock(), threat: [ @@ -636,10 +636,8 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); }); test('You can optionally send in an array of false positives', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 7e2da88a58f18..af3e427056867 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -157,49 +157,54 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription : `${singleThreat.tactic.name} (${singleThreat.tactic.id})`} - {singleThreat.technique.map((technique, techniqueIndex) => { - const myTechnique = techniquesOptions.find((t) => t.id === technique.id); - return ( - - - {myTechnique != null - ? myTechnique.label - : `${technique.name} (${technique.id})`} - - - {technique.subtechnique != null && - technique.subtechnique.map((subtechnique, subtechniqueIndex) => { - const mySubtechnique = subtechniquesOptions.find( - (t) => t.id === subtechnique.id - ); - return ( - - { + const myTechnique = techniquesOptions.find((t) => t.id === technique.id); + return ( + + + {myTechnique != null + ? myTechnique.label + : `${technique.name} (${technique.id})`} + + + {technique.subtechnique != null && + technique.subtechnique.map((subtechnique, subtechniqueIndex) => { + const mySubtechnique = subtechniquesOptions.find( + (t) => t.id === subtechnique.id + ); + return ( + - {mySubtechnique != null - ? mySubtechnique.label - : `${subtechnique.name} (${subtechnique.id})`} - - - ); - })} - - - ); - })} + + {mySubtechnique != null + ? mySubtechnique.label + : `${subtechnique.name} (${subtechnique.id})`} + + + ); + })} + + + ); + })} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx index da18f28257452..2a083ef89ab19 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx @@ -8,7 +8,7 @@ import { getValidThreat } from '../../../mitre/valid_threat_mock'; import { hasSubtechniqueOptions } from './helpers'; -const mockTechniques = getValidThreat()[0].technique; +const mockTechniques = getValidThreat()[0].technique ?? []; describe('helpers', () => { describe('hasSubtechniqueOptions', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx index e3c771534beda..d283c19bd13da 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx @@ -51,45 +51,46 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ const values = field.value as Threats; const technique = useMemo(() => { - return values[threatIndex].technique[techniqueIndex]; - }, [values, threatIndex, techniqueIndex]); + return [...(values[threatIndex].technique ?? [])]; + }, [values, threatIndex]); const removeSubtechnique = useCallback( (index: number) => { const threats = [...(field.value as Threats)]; - const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + const subtechniques = technique[techniqueIndex].subtechnique ?? []; if (subtechniques != null) { subtechniques.splice(index, 1); - threats[threatIndex].technique[techniqueIndex] = { - ...threats[threatIndex].technique[techniqueIndex], + technique[techniqueIndex] = { + ...technique[techniqueIndex], subtechnique: subtechniques, }; + threats[threatIndex].technique = technique; onFieldChange(threats); } }, - [field, threatIndex, onFieldChange, techniqueIndex] + [field, onFieldChange, techniqueIndex, technique, threatIndex] ); const addMitreAttackSubtechnique = useCallback(() => { const threats = [...(field.value as Threats)]; - const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + const subtechniques = technique[techniqueIndex].subtechnique; if (subtechniques != null) { - threats[threatIndex].technique[techniqueIndex] = { - ...threats[threatIndex].technique[techniqueIndex], + technique[techniqueIndex] = { + ...technique[techniqueIndex], subtechnique: [...subtechniques, { id: 'none', name: 'none', reference: 'none' }], }; } else { - threats[threatIndex].technique[techniqueIndex] = { - ...threats[threatIndex].technique[techniqueIndex], + technique[techniqueIndex] = { + ...technique[techniqueIndex], subtechnique: [{ id: 'none', name: 'none', reference: 'none' }], }; } - + threats[threatIndex].technique = technique; onFieldChange(threats); - }, [field, threatIndex, onFieldChange, techniqueIndex]); + }, [field, onFieldChange, techniqueIndex, technique, threatIndex]); const updateSubtechnique = useCallback( (index: number, value: string) => { @@ -99,7 +100,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ name: '', reference: '', }; - const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + const subtechniques = technique[techniqueIndex].subtechnique; if (subtechniques != null) { onFieldChange([ @@ -107,9 +108,9 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ { ...threats[threatIndex], technique: [ - ...threats[threatIndex].technique.slice(0, techniqueIndex), + ...technique.slice(0, techniqueIndex), { - ...threats[threatIndex].technique[techniqueIndex], + ...technique[techniqueIndex], subtechnique: [ ...subtechniques.slice(0, index), { @@ -120,19 +121,21 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ ...subtechniques.slice(index + 1), ], }, - ...threats[threatIndex].technique.slice(techniqueIndex + 1), + ...technique.slice(techniqueIndex + 1), ], }, ...threats.slice(threatIndex + 1), ]); } }, - [threatIndex, techniqueIndex, onFieldChange, field] + [threatIndex, techniqueIndex, onFieldChange, field, technique] ); const getSelectSubtechnique = useCallback( (index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => { - const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id); + const options = subtechniquesOptions.filter( + (t) => t.techniqueId === technique[techniqueIndex].id + ); return ( <> @@ -166,13 +169,17 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ ); }, - [field, updateSubtechnique, technique] + [field, updateSubtechnique, technique, techniqueIndex] ); + const subtechniques = useMemo(() => { + return technique[techniqueIndex].subtechnique; + }, [technique, techniqueIndex]); + return ( - {technique.subtechnique != null && - technique.subtechnique.map((subtechnique, index) => ( + {subtechniques != null && + subtechniques.map((subtechnique, index) => (
= ({ const removeTechnique = useCallback( (index: number) => { const threats = [...(field.value as Threats)]; - const techniques = threats[threatIndex].technique; + const techniques = threats[threatIndex].technique ?? []; techniques.splice(index, 1); threats[threatIndex] = { ...threats[threatIndex], @@ -73,7 +73,7 @@ export const MitreAttackTechniqueFields: React.FC = ({ threats[threatIndex] = { ...threats[threatIndex], technique: [ - ...threats[threatIndex].technique, + ...(threats[threatIndex].technique ?? []), { id: 'none', name: 'none', reference: 'none', subtechnique: [] }, ], }; @@ -88,19 +88,20 @@ export const MitreAttackTechniqueFields: React.FC = ({ name: '', reference: '', }; + const technique = threats[threatIndex].technique ?? []; onFieldChange([ ...threats.slice(0, threatIndex), { ...threats[threatIndex], technique: [ - ...threats[threatIndex].technique.slice(0, index), + ...technique.slice(0, index), { id, reference, name, subtechnique: [], }, - ...threats[threatIndex].technique.slice(index + 1), + ...technique.slice(index + 1), ], }, ...threats.slice(threatIndex + 1), @@ -147,9 +148,11 @@ export const MitreAttackTechniqueFields: React.FC = ({ [field, updateTechnique] ); + const techniques = values[threatIndex].technique ?? []; + return ( - {values[threatIndex].technique.map((technique, index) => ( + {techniques.map((technique, index) => (
{ .map((threat) => { return { ...threat, - technique: trimThreatsWithNoName(threat.technique).map((technique) => { + technique: trimThreatsWithNoName(threat.technique ?? []).map((technique) => { return { ...technique, subtechnique: