Skip to content

Commit

Permalink
[Security Solution] Coverage Overview follow-up 2 (elastic#164986)
Browse files Browse the repository at this point in the history
(cherry picked from commit 3835392)
  • Loading branch information
dplumlee committed Aug 28, 2023
1 parent c47cdbf commit 9a4cccb
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,6 @@ export const allowedExperimentalValues = Object.freeze({
**/
newUserDetailsFlyout: false,

/**
* Enables Protections/Detections Coverage Overview page (Epic link https://github.com/elastic/security-team/issues/2905)
*
* This flag aims to facilitate the development process as the feature may not make it to 8.10 release.
*
* The flag doesn't have to be documented and has to be removed after the feature is ready to release.
*/
detectionsCoverageOverview: true,

/**
* Enable risk engine client and initialisation of datastream, component templates and mappings
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ export const MlJobCompatibilityLink = () => (
linkText={i18n.ML_JOB_COMPATIBILITY_LINK_TEXT}
/>
);

export const CoverageOverviewLink = () => (
<DocLink docPath={i18n.COVERAGE_OVERVIEW_LINK_PATH} linkText={i18n.COVERAGE_OVERVIEW_LINK_TEXT} />
);
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,11 @@ export const ML_JOB_COMPATIBILITY_LINK_TEXT = i18n.translate(
defaultMessage: 'ML job compatibility',
}
);

export const COVERAGE_OVERVIEW_LINK_PATH = 'rules-coverage.html';
export const COVERAGE_OVERVIEW_LINK_TEXT = i18n.translate(
'xpack.securitySolution.documentationLinks.coverageOverview.text',
{
defaultMessage: 'Learn more.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import type { CoverageOverviewMitreSubTechnique } from '../../model/coverage_ove
import type { CoverageOverviewMitreTactic } from '../../model/coverage_overview/mitre_tactic';
import type { CoverageOverviewMitreTechnique } from '../../model/coverage_overview/mitre_technique';

// The order the tactic columns will appear in on the coverage overview page
const tacticOrder = [
'TA0043',
'TA0042',
'TA0001',
'TA0002',
'TA0003',
'TA0004',
'TA0005',
'TA0006',
'TA0007',
'TA0008',
'TA0009',
'TA0011',
'TA0010',
'TA0040',
];

export function buildCoverageOverviewMitreGraph(
tactics: MitreTactic[],
techniques: MitreTechnique[],
Expand Down Expand Up @@ -67,9 +85,13 @@ export function buildCoverageOverviewMitreGraph(
}
}

const sortedTactics = tactics.sort(
(a, b) => tacticOrder.indexOf(a.id) - tacticOrder.indexOf(b.id)
);

const result: CoverageOverviewMitreTactic[] = [];

for (const tactic of tactics) {
for (const tactic of sortedTactics) {
result.push({
id: tactic.id,
name: tactic.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { getTotalRuleCount } from './mitre_technique';
import { 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);
});

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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine';
import type { CoverageOverviewMitreSubTechnique } from './mitre_subtechnique';
import type { CoverageOverviewRule } from './rule';

Expand All @@ -20,3 +21,20 @@ export interface CoverageOverviewMitreTechnique {
disabledRules: CoverageOverviewRule[];
availableRules: CoverageOverviewRule[];
}

export const getTotalRuleCount = (
technique: CoverageOverviewMitreTechnique,
activity?: CoverageOverviewRuleActivity[]
): number => {
if (!activity) {
return technique.enabledRules.length + technique.disabledRules.length;
}
let totalRuleCount = 0;
if (activity.includes(CoverageOverviewRuleActivity.Enabled)) {
totalRuleCount += technique.enabledRules.length;
}
if (activity.includes(CoverageOverviewRuleActivity.Disabled)) {
totalRuleCount += technique.disabledRules.length;
}
return totalRuleCount;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import {
} from '../../../../../common/api/detection_engine';
import * as i18n from './translations';

export const coverageOverviewPaletteColors = ['#00BFB326', '#00BFB34D', '#00BFB399', '#00BFB3'];

export const coverageOverviewPanelWidth = 160;

export const coverageOverviewLegendWidth = 380;
Expand All @@ -25,10 +23,10 @@ export const coverageOverviewFilterWidth = 300;
* A corresponding color is applied if rules count >= a specific threshold
*/
export const coverageOverviewCardColorThresholds = [
{ threshold: 10, color: coverageOverviewPaletteColors[3] },
{ threshold: 7, color: coverageOverviewPaletteColors[2] },
{ threshold: 3, color: coverageOverviewPaletteColors[1] },
{ threshold: 1, color: coverageOverviewPaletteColors[0] },
{ threshold: 10, color: '#00BFB3' },
{ threshold: 7, color: '#00BFB399' },
{ threshold: 3, color: '#00BFB34D' },
{ threshold: 1, color: '#00BFB326' },
];

export const ruleActivityFilterDefaultOptions = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import { CoverageOverviewLink } from '../../../../common/components/links_to_docs';
import { HeaderPage } from '../../../../common/components/header_page';

import * as i18n from './translations';
Expand All @@ -14,26 +15,27 @@ import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_po
import { CoverageOverviewFiltersPanel } from './filters_panel';
import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context';

const CoverageOverviewHeaderComponent = () => (
<HeaderPage
title={i18n.COVERAGE_OVERVIEW_DASHBOARD_TITLE}
subtitle={
<EuiText color="subdued" size="s">
<span>{i18n.CoverageOverviewDashboardInformation}</span> <CoverageOverviewLink />
</EuiText>
}
/>
);

const CoverageOverviewHeader = React.memo(CoverageOverviewHeaderComponent);

const CoverageOverviewDashboardComponent = () => {
const {
state: { data },
} = useCoverageOverviewDashboardContext();
const subtitle = (
<EuiText color="subdued" size="s">
<span>{i18n.CoverageOverviewDashboardInformation}</span>{' '}
<EuiLink
external={true}
href={'https://www.elastic.co/'} // TODO: change to actual docs link before release
rel="noopener noreferrer"
target="_blank"
>
{i18n.CoverageOverviewDashboardInformationLink}
</EuiLink>
</EuiText>
);

return (
<>
<HeaderPage title={i18n.COVERAGE_OVERVIEW_DASHBOARD_TITLE} subtitle={subtitle} />
<CoverageOverviewHeader />
<CoverageOverviewFiltersPanel />
<EuiSpacer />
<EuiFlexGroup gutterSize="m" className="eui-xScroll">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine';
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,
Expand All @@ -17,7 +17,6 @@ import {
extractSelected,
getNumOfCoveredSubtechniques,
getNumOfCoveredTechniques,
getTotalRuleCount,
populateSelected,
} from './helpers';

Expand Down Expand Up @@ -89,26 +88,4 @@ describe('helpers', () => {
]);
});
});

describe('getTotalRuleCount', () => {
it('returns count of all rules when no activity filter is present', () => {
const payload = getMockCoverageOverviewMitreTechnique();
expect(getTotalRuleCount(payload)).toEqual(2);
});

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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
*/

import type { EuiSelectableOption } from '@elastic/eui';
import type { CoverageOverviewRuleSource } from '../../../../../common/api/detection_engine';
import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine';
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';
Expand Down Expand Up @@ -41,20 +43,3 @@ export const populateSelected = (
allOptions.map((option) =>
selected.includes(option.label) ? { ...option, checked: 'on' } : option
);

export const getTotalRuleCount = (
technique: CoverageOverviewMitreTechnique,
activity?: CoverageOverviewRuleActivity[]
): number => {
if (!activity) {
return technique.enabledRules.length + technique.disabledRules.length;
}
let totalRuleCount = 0;
if (activity.includes(CoverageOverviewRuleActivity.Enabled)) {
totalRuleCount += technique.enabledRules.length;
}
if (activity.includes(CoverageOverviewRuleActivity.Disabled)) {
totalRuleCount += technique.disabledRules.length;
}
return totalRuleCount;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { css } from '@emotion/css';
import React, { memo, useCallback, useMemo } from 'react';
import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique';
import { getTotalRuleCount } from '../../../rule_management/model/coverage_overview/mitre_technique';
import { coverageOverviewPanelWidth } from './constants';
import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context';
import { getCardBackgroundColor, getTotalRuleCount } from './helpers';
import { getCardBackgroundColor } from './helpers';
import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats';
import * as i18n from './translations';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
export const COVERAGE_OVERVIEW_DASHBOARD_TITLE = i18n.translate(
'xpack.securitySolution.coverageOverviewDashboard.pageTitle',
{
defaultMessage: 'MITRE ATT&CK\u00AE Coverage',
defaultMessage: 'MITRE ATT&CK\u00AE coverage',
}
);

Expand Down Expand Up @@ -174,13 +174,6 @@ export const CoverageOverviewDashboardInformation = i18n.translate(
'xpack.securitySolution.coverageOverviewDashboard.dashboardInformation',
{
defaultMessage:
'The interactive MITRE ATT&CK coverage below shows the current state of your coverage from installed rules, click on a cell to view further details. Unmapped rules will not be displayed. View further information from our',
}
);

export const CoverageOverviewDashboardInformationLink = i18n.translate(
'xpack.securitySolution.coverageOverviewDashboard.dashboardInformationLink',
{
defaultMessage: 'docs.',
"Your current coverage of MITRE ATT&CK\u00AE tactics and techniques, based on installed rules. Click a cell to view and enable a technique's rules. Rules must be mapped to the MITRE ATT&CK\u00AE framework to be displayed.",
}
);
1 change: 0 additions & 1 deletion x-pack/plugins/security_solution/public/rules/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ export const links: LinkItem = {
defaultMessage: 'MITRE ATT&CK Coverage',
}),
],
experimentalKey: 'detectionsCoverageOverview',
},
],
categories: [
Expand Down
23 changes: 7 additions & 16 deletions x-pack/plugins/security_solution/public/rules/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { AllRulesTabs } from '../detection_engine/rule_management_ui/components/
import { AddRulesPage } from '../detection_engine/rule_management_ui/pages/add_rules';
import type { SecuritySubPluginRoutes } from '../app/types';
import { RulesLandingPage } from './landing';
import { useIsExperimentalFeatureEnabled } from '../common/hooks/use_experimental_features';
import { CoverageOverviewPage } from '../detection_engine/rule_management_ui/pages/coverage_overview';

const RulesSubRoutes = [
Expand Down Expand Up @@ -109,21 +108,13 @@ const RulesContainerComponent: React.FC = () => {

const Rules = React.memo(RulesContainerComponent);

const CoverageOverviewRoutes = () => {
const isDetectionsCoverageOverviewEnabled = useIsExperimentalFeatureEnabled(
'detectionsCoverageOverview'
);

return isDetectionsCoverageOverviewEnabled ? (
<PluginTemplateWrapper>
<TrackApplicationView viewId={SecurityPageName.coverageOverview}>
<CoverageOverviewPage />
</TrackApplicationView>
</PluginTemplateWrapper>
) : (
<Redirect to={SecurityPageName.landing} />
);
};
const CoverageOverviewRoutes = () => (
<PluginTemplateWrapper>
<TrackApplicationView viewId={SecurityPageName.coverageOverview}>
<CoverageOverviewPage />
</TrackApplicationView>
</PluginTemplateWrapper>
);

export const routes: SecuritySubPluginRoutes = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ export const registerRuleManagementRoutes = (
// Rules filters
getRuleManagementFilters(router);

// Rules dashboard
if (config.experimentalFeatures.detectionsCoverageOverview) {
getCoverageOverviewRoute(router);
}
// Rules coverage overview
getCoverageOverviewRoute(router);
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s
'previewTelemetryUrlEnabled',
'riskScoringPersistence',
'riskScoringRoutesEnabled',
'detectionsCoverageOverview',
])}`,
'--xpack.task_manager.poll_interval=1000',
`--xpack.actions.preconfigured=${JSON.stringify({
Expand Down

0 comments on commit 9a4cccb

Please sign in to comment.