Skip to content

Commit

Permalink
[Security Solution] Changes coverage overview subtechnique display to…
Browse files Browse the repository at this point in the history
… base off active filters (#170988)
  • Loading branch information
dplumlee authored Nov 14, 2023
1 parent 603a045 commit 87ec144
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -38,3 +39,6 @@ export const getTotalRuleCount = (
}
return totalRuleCount;
};

export const getNumOfCoveredTechniques = (tactic: CoverageOverviewMitreTactic): number =>
tactic.techniques.filter((technique) => technique.enabledRules.length !== 0).length;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]
Expand All @@ -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);
Expand Down

0 comments on commit 87ec144

Please sign in to comment.