diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.test.ts new file mode 100644 index 0000000000000..7129448e762cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { getNumOfCoveredSubtechniques } from './mitre_subtechnique'; +import type { CoverageOverviewMitreTechnique } from './mitre_technique'; +import { + getMockCoverageOverviewMitreSubTechnique, + getMockCoverageOverviewMitreTechnique, +} from './__mocks__'; + +describe('mitre_subtechniques', () => { + describe('getNumOfCoveredSubtechniques', () => { + it('returns 0 when no subtechniques are present', () => { + const payload: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(); + expect(getNumOfCoveredSubtechniques(payload)).toEqual(0); + }); + + it('returns total number of unique enabled and disabled subtechniques when no filter is passed', () => { + const payload: CoverageOverviewMitreTechnique = { + ...getMockCoverageOverviewMitreTechnique(), + subtechniques: [ + getMockCoverageOverviewMitreSubTechnique(), + { ...getMockCoverageOverviewMitreSubTechnique(), id: 'test-id' }, + ], + }; + expect(getNumOfCoveredSubtechniques(payload)).toEqual(2); + }); + + it('returns total number of unique enabled and disabled subtechniques when both filters are passed', () => { + const payload: CoverageOverviewMitreTechnique = { + ...getMockCoverageOverviewMitreTechnique(), + subtechniques: [ + getMockCoverageOverviewMitreSubTechnique(), + { ...getMockCoverageOverviewMitreSubTechnique(), id: 'test-id' }, + ], + }; + expect( + getNumOfCoveredSubtechniques(payload, [ + CoverageOverviewRuleActivity.Enabled, + CoverageOverviewRuleActivity.Disabled, + ]) + ).toEqual(2); + }); + + it('returns total number of enabled subtechniques when enabled filter is passed', () => { + const payload: CoverageOverviewMitreTechnique = { + ...getMockCoverageOverviewMitreTechnique(), + subtechniques: [ + { + ...getMockCoverageOverviewMitreSubTechnique(), + enabledRules: [], + }, + getMockCoverageOverviewMitreSubTechnique(), + ], + }; + expect(getNumOfCoveredSubtechniques(payload, [CoverageOverviewRuleActivity.Enabled])).toEqual( + 1 + ); + }); + + it('returns total number of disabled subtechniques when disabled filter is passed', () => { + const payload: CoverageOverviewMitreTechnique = { + ...getMockCoverageOverviewMitreTechnique(), + subtechniques: [ + { + ...getMockCoverageOverviewMitreSubTechnique(), + disabledRules: [], + }, + getMockCoverageOverviewMitreSubTechnique(), + ], + }; + expect( + getNumOfCoveredSubtechniques(payload, [CoverageOverviewRuleActivity.Disabled]) + ).toEqual(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.ts index 622213c7e7a6f..0b0e8af2d8a99 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_subtechnique.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewMitreTechnique } from './mitre_technique'; import type { CoverageOverviewRule } from './rule'; export interface CoverageOverviewMitreSubTechnique { @@ -18,3 +20,26 @@ export interface CoverageOverviewMitreSubTechnique { disabledRules: CoverageOverviewRule[]; availableRules: CoverageOverviewRule[]; } + +export const getNumOfCoveredSubtechniques = ( + technique: CoverageOverviewMitreTechnique, + activity?: CoverageOverviewRuleActivity[] +): number => { + const coveredSubtechniques = new Set(); + for (const subtechnique of technique.subtechniques) { + if ( + (!activity || activity.includes(CoverageOverviewRuleActivity.Enabled)) && + subtechnique.enabledRules.length + ) { + coveredSubtechniques.add(subtechnique.id); + } + + if ( + (!activity || activity.includes(CoverageOverviewRuleActivity.Disabled)) && + subtechnique.disabledRules.length + ) { + coveredSubtechniques.add(subtechnique.id); + } + } + return coveredSubtechniques.size; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.test.ts index 01794b34c8ee7..9a89867225ba8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.test.ts @@ -6,27 +6,52 @@ */ import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; -import { getTotalRuleCount } from './mitre_technique'; -import { getMockCoverageOverviewMitreTechnique } from './__mocks__'; +import type { CoverageOverviewMitreTactic } from './mitre_tactic'; +import type { CoverageOverviewMitreTechnique } from './mitre_technique'; +import { getNumOfCoveredTechniques, getTotalRuleCount } from './mitre_technique'; +import { + getMockCoverageOverviewMitreTactic, + getMockCoverageOverviewMitreTechnique, +} from './__mocks__'; -describe('getTotalRuleCount', () => { - it('returns count of all rules when no activity filter is present', () => { - const payload = getMockCoverageOverviewMitreTechnique(); - expect(getTotalRuleCount(payload)).toEqual(2); - }); +describe('mitre_technique', () => { + describe('getTotalRuleCount', () => { + it('returns count of all rules when no activity filter is present', () => { + const payload: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(); + expect(getTotalRuleCount(payload)).toEqual(2); + }); + + it('returns count of one rule type when an activity filter is present', () => { + const payload: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(); + expect(getTotalRuleCount(payload, [CoverageOverviewRuleActivity.Disabled])).toEqual(1); + }); - it('returns count of one rule type when an activity filter is present', () => { - const payload = getMockCoverageOverviewMitreTechnique(); - expect(getTotalRuleCount(payload, [CoverageOverviewRuleActivity.Disabled])).toEqual(1); + it('returns count of multiple rule type when multiple activity filter is present', () => { + const payload = getMockCoverageOverviewMitreTechnique(); + expect( + getTotalRuleCount(payload, [ + CoverageOverviewRuleActivity.Enabled, + CoverageOverviewRuleActivity.Disabled, + ]) + ).toEqual(2); + }); }); - it('returns count of multiple rule type when multiple activity filter is present', () => { - const payload = getMockCoverageOverviewMitreTechnique(); - expect( - getTotalRuleCount(payload, [ - CoverageOverviewRuleActivity.Enabled, - CoverageOverviewRuleActivity.Disabled, - ]) - ).toEqual(2); + describe('getNumOfCoveredTechniques', () => { + it('returns 0 when no techniques are present', () => { + const payload: CoverageOverviewMitreTactic = getMockCoverageOverviewMitreTactic(); + expect(getNumOfCoveredTechniques(payload)).toEqual(0); + }); + + it('returns number of techniques when present', () => { + const payload: CoverageOverviewMitreTactic = { + ...getMockCoverageOverviewMitreTactic(), + techniques: [ + getMockCoverageOverviewMitreTechnique(), + getMockCoverageOverviewMitreTechnique(), + ], + }; + expect(getNumOfCoveredTechniques(payload)).toEqual(2); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.ts index 589629d643810..23f57497d7d7d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/mitre_technique.ts @@ -7,6 +7,7 @@ import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; import type { CoverageOverviewMitreSubTechnique } from './mitre_subtechnique'; +import type { CoverageOverviewMitreTactic } from './mitre_tactic'; import type { CoverageOverviewRule } from './rule'; export interface CoverageOverviewMitreTechnique { @@ -38,3 +39,6 @@ export const getTotalRuleCount = ( } return totalRuleCount; }; + +export const getNumOfCoveredTechniques = (tactic: CoverageOverviewMitreTactic): number => + tactic.techniques.filter((technique) => technique.enabledRules.length !== 0).length; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts index 5a1aee424352a..39e0b9d7bbb53 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts @@ -7,56 +7,10 @@ import type { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; import { getCoverageOverviewFilterMock } from '../../../../../common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock'; -import { - getMockCoverageOverviewMitreSubTechnique, - getMockCoverageOverviewMitreTactic, - getMockCoverageOverviewMitreTechnique, -} from '../../../rule_management/model/coverage_overview/__mocks__'; import { ruleActivityFilterDefaultOptions } from './constants'; -import { - extractSelected, - getNumOfCoveredSubtechniques, - getNumOfCoveredTechniques, - populateSelected, -} from './helpers'; +import { extractSelected, populateSelected } from './helpers'; describe('helpers', () => { - describe('getNumOfCoveredTechniques', () => { - it('returns 0 when no techniques are present', () => { - const payload = getMockCoverageOverviewMitreTactic(); - expect(getNumOfCoveredTechniques(payload)).toEqual(0); - }); - - it('returns number of techniques when present', () => { - const payload = { - ...getMockCoverageOverviewMitreTactic(), - techniques: [ - getMockCoverageOverviewMitreTechnique(), - getMockCoverageOverviewMitreTechnique(), - ], - }; - expect(getNumOfCoveredTechniques(payload)).toEqual(2); - }); - }); - - describe('getNumOfCoveredSubtechniques', () => { - it('returns 0 when no subtechniques are present', () => { - const payload = getMockCoverageOverviewMitreTechnique(); - expect(getNumOfCoveredSubtechniques(payload)).toEqual(0); - }); - - it('returns number of subtechniques when present', () => { - const payload = { - ...getMockCoverageOverviewMitreTechnique(), - subtechniques: [ - getMockCoverageOverviewMitreSubTechnique(), - getMockCoverageOverviewMitreSubTechnique(), - ], - }; - expect(getNumOfCoveredSubtechniques(payload)).toEqual(2); - }); - }); - describe('extractSelected', () => { it('returns empty array when no options are checked', () => { const payload = ruleActivityFilterDefaultOptions; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts index 82d50e7b9721b..ecd67546e7627 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts @@ -10,16 +10,8 @@ import type { CoverageOverviewRuleActivity, CoverageOverviewRuleSource, } from '../../../../../common/api/detection_engine'; -import type { CoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/mitre_tactic'; -import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { coverageOverviewCardColorThresholds } from './constants'; -export const getNumOfCoveredTechniques = (tactic: CoverageOverviewMitreTactic): number => - tactic.techniques.filter((technique) => technique.enabledRules.length !== 0).length; - -export const getNumOfCoveredSubtechniques = (technique: CoverageOverviewMitreTechnique): number => - technique.subtechniques.filter((subtechnique) => subtechnique.enabledRules.length !== 0).length; - export const getCardBackgroundColor = (value: number) => { for (const { threshold, color } of coverageOverviewCardColorThresholds) { if (value >= threshold) { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx index e1d1749ca264f..64ef73c8259c9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx @@ -11,9 +11,9 @@ import React, { memo, useMemo } from 'react'; import { euiThemeVars } from '@kbn/ui-theme'; import type { CoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/mitre_tactic'; import { coverageOverviewPanelWidth } from './constants'; -import { getNumOfCoveredTechniques } from './helpers'; import * as i18n from './translations'; import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats'; +import { getNumOfCoveredTechniques } from '../../../rule_management/model/coverage_overview/mitre_technique'; export interface CoverageOverviewTacticPanelProps { tactic: CoverageOverviewMitreTactic; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx index f5fc71b08b055..9e026e9912b46 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx @@ -23,12 +23,12 @@ import { css, cx } from '@emotion/css'; import React, { memo, useCallback, useMemo, useState } from 'react'; import { useUserData } from '../../../../detections/components/user_info'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; -import { getNumOfCoveredSubtechniques } from './helpers'; import { CoverageOverviewRuleListHeader } from './shared_components/popover_list_header'; import { CoverageOverviewMitreTechniquePanel } from './technique_panel'; import * as i18n from './translations'; import { RuleLink } from '../../components/rules_table/use_columns'; import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; +import { getNumOfCoveredSubtechniques } from '../../../rule_management/model/coverage_overview/mitre_subtechnique'; export interface CoverageOverviewMitreTechniquePanelPopoverProps { technique: CoverageOverviewMitreTechnique; @@ -41,7 +41,6 @@ const CoverageOverviewMitreTechniquePanelPopoverComponent = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const coveredSubtechniques = useMemo(() => getNumOfCoveredSubtechniques(technique), [technique]); const isEnableButtonDisabled = useMemo( () => !canUserCRUD || technique.disabledRules.length === 0, [canUserCRUD, technique.disabledRules.length] @@ -53,10 +52,18 @@ const CoverageOverviewMitreTechniquePanelPopoverComponent = ({ ); const { - state: { showExpandedCells }, + state: { + showExpandedCells, + filter: { activity }, + }, actions: { enableAllDisabled }, } = useCoverageOverviewDashboardContext(); + const coveredSubtechniques = useMemo( + () => getNumOfCoveredSubtechniques(technique, activity), + [activity, technique] + ); + const handleEnableAllDisabled = useCallback(async () => { setIsLoading(true); const ruleIds = technique.disabledRules.map((rule) => rule.id);