mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# 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:
parent
91afdf16c1
commit
7699da3f7f
16 changed files with 124 additions and 106 deletions
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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.",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -104,7 +104,6 @@ export const links: LinkItem = {
|
|||
defaultMessage: 'MITRE ATT&CK Coverage',
|
||||
}),
|
||||
],
|
||||
experimentalKey: 'detectionsCoverageOverview',
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -62,8 +62,6 @@ export const registerRuleManagementRoutes = (
|
|||
// Rules filters
|
||||
getRuleManagementFilters(router);
|
||||
|
||||
// Rules dashboard
|
||||
if (config.experimentalFeatures.detectionsCoverageOverview) {
|
||||
getCoverageOverviewRoute(router);
|
||||
}
|
||||
// Rules coverage overview
|
||||
getCoverageOverviewRoute(router);
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue