Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Coverage Overview follow-up 2 #164986

Merged
merged 4 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Oh interesting, this shows that link text sometimes can be very context-specific, and ideally should be passed as a property to the CoverageOverviewLink and other link components. Just something we could refactor in the future.

}
);
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 @@ -104,7 +104,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