From 7f09902d149902b148b9c4c48247cf73c0e18376 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:36:01 -0500 Subject: [PATCH] [Security Solution] Coverage overview rule duplication fix (#169708) (cherry picked from commit 2beb8f7965049205913ad39711f106bb2f844b8a) --- ..._coverage_overview_dashboard_model.test.ts | 288 ++++++++++++++++++ ...build_coverage_overview_dashboard_model.ts | 8 +- ...uild_coverage_overview_mitre_graph.test.ts | 65 +--- .../build_coverage_overview_mitre_graph.ts | 19 +- .../coverage_overview/__mocks__/index.ts | 57 ++++ 5 files changed, 370 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts new file mode 100644 index 0000000000000..01e57bc17948b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts @@ -0,0 +1,288 @@ +/* + * 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 { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewResponse } from '../../../../../common/api/detection_engine'; +import { buildCoverageOverviewDashboardModel } from './build_coverage_overview_dashboard_model'; +import { + getMockCoverageOverviewSubtechniques, + getMockCoverageOverviewTactics, + getMockCoverageOverviewTechniques, +} from '../../model/coverage_overview/__mocks__'; + +const mockTactics = getMockCoverageOverviewTactics(); +const mockTechniques = getMockCoverageOverviewTechniques(); +const mockSubtechniques = getMockCoverageOverviewSubtechniques(); + +describe('buildCoverageOverviewDashboardModel', () => { + beforeEach(() => { + jest.mock('../../../../detections/mitre/mitre_tactics_techniques', () => { + return { + tactics: mockTactics, + techniques: mockTechniques, + subtechniques: mockSubtechniques, + }; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('maps API response', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1'], + TA002: ['test-rule-1'], + T001: ['test-rule-1'], + T002: ['test-rule-1'], + 'T001.001': ['test-rule-1'], + }, + unmapped_rule_ids: ['test-rule-2'], + rules_data: { + 'test-rule-1': { + name: 'Test rule 1', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model).toEqual({ + metrics: { + totalEnabledRulesCount: 2, + totalRulesCount: 2, + }, + mitreTactics: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'TA001', + name: 'Tactic 1', + reference: 'https://some-link/TA001', + techniques: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T001', + name: 'Technique 1', + reference: 'https://some-link/T001', + subtechniques: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T001.001', + name: 'Subtechnique 1', + reference: 'https://some-link/T001/001', + }, + { + availableRules: [], + disabledRules: [], + enabledRules: [], + id: 'T001.002', + name: 'Subtechnique 2', + reference: 'https://some-link/T001/002', + }, + ], + }, + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T002', + name: 'Technique 2', + reference: 'https://some-link/T002', + subtechniques: [], + }, + ], + }, + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'TA002', + name: 'Tactic 2', + reference: 'https://some-link/TA002', + techniques: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T002', + name: 'Technique 2', + reference: 'https://some-link/T002', + subtechniques: [], + }, + ], + }, + ], + unmappedRules: { + availableRules: [], + disabledRules: [], + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + }, + }); + }); + + it('maps techniques that appear in multiple tactics', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1', 'test-rule-2'], + TA002: ['test-rule-2'], + T002: ['test-rule-1', 'test-rule-2'], + }, + unmapped_rule_ids: [], + rules_data: { + 'test-rule-1': { + name: 'Test rule', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model.mitreTactics).toEqual([ + expect.objectContaining({ + id: 'TA001', + enabledRules: [ + expect.objectContaining({ id: 'test-rule-1' }), + expect.objectContaining({ id: 'test-rule-2' }), + ], + techniques: [ + expect.objectContaining({ id: 'T001', enabledRules: [] }), + expect.objectContaining({ + id: 'T002', + enabledRules: [ + expect.objectContaining({ id: 'test-rule-1' }), + expect.objectContaining({ id: 'test-rule-2' }), + ], + }), + ], + }), + expect.objectContaining({ + id: 'TA002', + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + techniques: [ + expect.objectContaining({ + id: 'T002', + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + }), + ], + }), + ]); + }); + + it('maps unmapped rules', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1'], + T002: ['test-rule-1'], + }, + unmapped_rule_ids: ['test-rule-2'], + rules_data: { + 'test-rule-1': { + name: 'Test rule 1', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model.unmappedRules).toEqual({ + availableRules: [], + disabledRules: [], + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + }); + expect(model.mitreTactics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'TA001', + enabledRules: expect.not.arrayContaining(['test-rule-2']), + }), + ]) + ); + }); + + it('maps metrics fields', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1'], + T002: ['test-rule-1'], + }, + unmapped_rule_ids: ['test-rule-2'], + rules_data: { + 'test-rule-1': { + name: 'Test rule 1', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-3': { + name: 'Test rule 3', + activity: CoverageOverviewRuleActivity.Disabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model.metrics).toEqual({ + totalEnabledRulesCount: 2, + totalRulesCount: 3, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts index 04d9c2e4cf4d0..fd0055d480e0b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts @@ -40,12 +40,16 @@ export async function buildCoverageOverviewDashboardModel( for (const technique of tactic.techniques) { for (const ruleId of apiResponse.coverage[technique.id] ?? []) { - addRule(technique, ruleId, apiResponse.rules_data[ruleId]); + if (apiResponse.coverage[tactic.id]?.includes(ruleId)) { + addRule(technique, ruleId, apiResponse.rules_data[ruleId]); + } } for (const subtechnique of technique.subtechniques) { for (const ruleId of apiResponse.coverage[subtechnique.id] ?? []) { - addRule(subtechnique, ruleId, apiResponse.rules_data[ruleId]); + if (apiResponse.coverage[tactic.id]?.includes(ruleId)) { + addRule(subtechnique, ruleId, apiResponse.rules_data[ruleId]); + } } } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts index 3a95160c96356..edb56fd2d3d64 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts @@ -5,66 +5,19 @@ * 2.0. */ +import { + getMockCoverageOverviewTactics, + getMockCoverageOverviewTechniques, + getMockCoverageOverviewSubtechniques, +} from '../../model/coverage_overview/__mocks__'; import { buildCoverageOverviewMitreGraph } from './build_coverage_overview_mitre_graph'; describe('buildCoverageOverviewModel', () => { it('builds domain model', () => { - const tactics = [ - { - name: 'Tactic 1', - id: 'TA001', - reference: 'https://some-link/TA001', - label: 'Tactic 1', - value: 'tactic1', - }, - { - name: 'Tactic 2', - id: 'TA002', - reference: 'https://some-link/TA002', - label: 'Tactic 2', - value: 'tactic2', - }, - ]; - const techniques = [ - { - name: 'Technique 1', - id: 'T001', - reference: 'https://some-link/T001', - tactics: ['tactic-1'], - label: 'Technique 1', - value: 'technique1', - }, - { - name: 'Technique 2', - id: 'T002', - reference: 'https://some-link/T002', - tactics: ['tactic-1', 'tactic-2'], - label: 'Technique 2', - value: 'technique2', - }, - ]; - const subtechniques = [ - { - name: 'Subtechnique 1', - id: 'T001.001', - reference: 'https://some-link/T001/001', - tactics: ['tactic-1'], - techniqueId: 'T001', - label: 'Subtechnique 1', - value: 'subtechnique1', - }, - { - name: 'Subtechnique 2', - id: 'T001.002', - reference: 'https://some-link/T001/002', - tactics: ['tactic-1'], - techniqueId: 'T001', - label: 'Subtechnique 2', - value: 'subtechnique2', - }, - ]; - - const model = buildCoverageOverviewMitreGraph(tactics, techniques, subtechniques); + const mockTactics = getMockCoverageOverviewTactics(); + const mockTechniques = getMockCoverageOverviewTechniques(); + const mockSubtechniques = getMockCoverageOverviewSubtechniques(); + const model = buildCoverageOverviewMitreGraph(mockTactics, mockTechniques, mockSubtechniques); expect(model).toEqual([ { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts index acae46d0f3701..51c14661ddba1 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts @@ -64,17 +64,18 @@ export function buildCoverageOverviewMitreGraph( const tacticToTechniquesMap = new Map(); // Map(kebabCase(tactic name) -> CoverageOverviewMitreTechnique) for (const technique of techniques) { - const coverageOverviewMitreTechnique: CoverageOverviewMitreTechnique = { - id: technique.id, - name: technique.name, - reference: technique.reference, - subtechniques: techniqueToSubtechniquesMap.get(technique.id) ?? [], - enabledRules: [], - disabledRules: [], - availableRules: [], - }; + const relatedSubtechniques = techniqueToSubtechniquesMap.get(technique.id) ?? []; for (const kebabCaseTacticName of technique.tactics) { + const coverageOverviewMitreTechnique: CoverageOverviewMitreTechnique = { + id: technique.id, + name: technique.name, + reference: technique.reference, + subtechniques: relatedSubtechniques, + enabledRules: [], + disabledRules: [], + availableRules: [], + }; const tacticTechniques = tacticToTechniquesMap.get(kebabCaseTacticName); if (!tacticTechniques) { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts index 21a32787aa5db..d6751152b77a9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts @@ -57,3 +57,60 @@ export const getMockCoverageOverviewDashboard = (): CoverageOverviewDashboard => totalEnabledRulesCount: 1, }, }); + +export const getMockCoverageOverviewTactics = () => [ + { + name: 'Tactic 1', + id: 'TA001', + reference: 'https://some-link/TA001', + label: 'Tactic 1', + value: 'tactic1', + }, + { + name: 'Tactic 2', + id: 'TA002', + reference: 'https://some-link/TA002', + label: 'Tactic 2', + value: 'tactic2', + }, +]; + +export const getMockCoverageOverviewTechniques = () => [ + { + name: 'Technique 1', + id: 'T001', + reference: 'https://some-link/T001', + tactics: ['tactic-1'], + label: 'Technique 1', + value: 'technique1', + }, + { + name: 'Technique 2', + id: 'T002', + reference: 'https://some-link/T002', + tactics: ['tactic-1', 'tactic-2'], + label: 'Technique 2', + value: 'technique2', + }, +]; + +export const getMockCoverageOverviewSubtechniques = () => [ + { + name: 'Subtechnique 1', + id: 'T001.001', + reference: 'https://some-link/T001/001', + tactics: ['tactic-1'], + techniqueId: 'T001', + label: 'Subtechnique 1', + value: 'subtechnique1', + }, + { + name: 'Subtechnique 2', + id: 'T001.002', + reference: 'https://some-link/T001/002', + tactics: ['tactic-1'], + techniqueId: 'T001', + label: 'Subtechnique 2', + value: 'subtechnique2', + }, +];