[8.10] [Security Solution] Coverage Overview follow-up 2 (#164986) (#165045)

# Backport

This will backport the following commits from `main` to `8.10`:
- [[Security Solution] Coverage Overview follow-up 2
(#164986)](https://github.com/elastic/kibana/pull/164986)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Davis
Plumlee","email":"56367316+dplumlee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-08-28T20:39:20Z","message":"[Security
Solution] Coverage Overview follow-up 2
(#164986)","sha":"3835392e329b1a3cc1dba0a1b6f36a36a87c1cfa","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:Detections
and Resp","Team: SecuritySolution","Feature:Rule
Management","Team:Detection Rule
Management","v8.10.0","v8.11.0"],"number":164986,"url":"https://github.com/elastic/kibana/pull/164986","mergeCommit":{"message":"[Security
Solution] Coverage Overview follow-up 2
(#164986)","sha":"3835392e329b1a3cc1dba0a1b6f36a36a87c1cfa"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164986","number":164986,"mergeCommit":{"message":"[Security
Solution] Coverage Overview follow-up 2
(#164986)","sha":"3835392e329b1a3cc1dba0a1b6f36a36a87c1cfa"}}]}]
BACKPORT-->

Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-08-29 16:04:51 -04:00 committed by GitHub
parent 91afdf16c1
commit 7699da3f7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 124 additions and 106 deletions

View file

@ -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
*/

View file

@ -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} />
);

View file

@ -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.',
}
);

View file

@ -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[],
@ -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,

View file

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

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine';
import type { CoverageOverviewMitreSubTechnique } from './mitre_subtechnique';
import type { CoverageOverviewRule } from './rule';
@ -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;
};

View file

@ -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;
@ -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 = [

View file

@ -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';
@ -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">

View file

@ -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,
@ -17,7 +17,6 @@ import {
extractSelected,
getNumOfCoveredSubtechniques,
getNumOfCoveredTechniques,
getTotalRuleCount,
populateSelected,
} from './helpers';
@ -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);
});
});
});

View file

@ -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';
@ -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;
};

View file

@ -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';

View file

@ -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',
}
);
@ -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.",
}
);

View file

@ -104,7 +104,6 @@ export const links: LinkItem = {
defaultMessage: 'MITRE ATT&CK Coverage',
}),
],
experimentalKey: 'detectionsCoverageOverview',
},
],
categories: [

View file

@ -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 = [
@ -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 = [
{

View file

@ -62,8 +62,6 @@ export const registerRuleManagementRoutes = (
// Rules filters
getRuleManagementFilters(router);
// Rules dashboard
if (config.experimentalFeatures.detectionsCoverageOverview) {
getCoverageOverviewRoute(router);
}
// Rules coverage overview
getCoverageOverviewRoute(router);
};

View file

@ -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({