mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Coverage Overview follow-up (#164613)
This commit is contained in:
parent
b5b2c36d95
commit
168e3dc5e9
16 changed files with 181 additions and 78 deletions
|
@ -15,7 +15,6 @@ import {
|
|||
detectionResponseLinks,
|
||||
entityAnalyticsLinks,
|
||||
overviewLinks,
|
||||
coverageOverviewDashboardLinks,
|
||||
} from '../overview/links';
|
||||
import { IconDashboards } from '../common/icons/dashboards';
|
||||
|
||||
|
@ -27,7 +26,6 @@ const subLinks: LinkItem[] = [
|
|||
vulnerabilityDashboardLink,
|
||||
entityAnalyticsLinks,
|
||||
ecsDataQualityDashboardLinks,
|
||||
coverageOverviewDashboardLinks,
|
||||
].map((link) => ({ ...link, sideNavIcon: IconDashboards }));
|
||||
|
||||
export const dashboardsLinks: LinkItem = {
|
||||
|
|
|
@ -5,14 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { euiPalettePositive } from '@elastic/eui';
|
||||
import {
|
||||
CoverageOverviewRuleActivity,
|
||||
CoverageOverviewRuleSource,
|
||||
} from '../../../../../common/api/detection_engine';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const coverageOverviewPaletteColors = euiPalettePositive(5);
|
||||
export const coverageOverviewPaletteColors = ['#00BFB326', '#00BFB34D', '#00BFB399', '#00BFB3'];
|
||||
|
||||
export const coverageOverviewPanelWidth = 160;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { HeaderPage } from '../../../../common/components/header_page';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
@ -18,9 +18,22 @@ 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} />
|
||||
<HeaderPage title={i18n.COVERAGE_OVERVIEW_DASHBOARD_TITLE} subtitle={subtitle} />
|
||||
<CoverageOverviewFiltersPanel />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup gutterSize="m" className="eui-xScroll">
|
||||
|
|
|
@ -14,11 +14,11 @@ import React, {
|
|||
useReducer,
|
||||
} from 'react';
|
||||
import { invariant } from '../../../../../common/utils/invariant';
|
||||
import type {
|
||||
import {
|
||||
BulkActionType,
|
||||
CoverageOverviewRuleActivity,
|
||||
CoverageOverviewRuleSource,
|
||||
} from '../../../../../common/api/detection_engine';
|
||||
import { BulkActionType } from '../../../../../common/api/detection_engine';
|
||||
import type { CoverageOverviewDashboardState } from './coverage_overview_dashboard_reducer';
|
||||
import {
|
||||
SET_SHOW_EXPANDED_CELLS,
|
||||
|
@ -53,7 +53,10 @@ interface CoverageOverviewDashboardContextProviderProps {
|
|||
|
||||
export const initialState: CoverageOverviewDashboardState = {
|
||||
showExpandedCells: false,
|
||||
filter: {},
|
||||
filter: {
|
||||
activity: [CoverageOverviewRuleActivity.Enabled],
|
||||
source: [CoverageOverviewRuleSource.Prebuilt, CoverageOverviewRuleSource.Custom],
|
||||
},
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine';
|
||||
import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine';
|
||||
import { getCoverageOverviewFilterMock } from '../../../../../common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock';
|
||||
import {
|
||||
getMockCoverageOverviewMitreSubTechnique,
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
extractSelected,
|
||||
getNumOfCoveredSubtechniques,
|
||||
getNumOfCoveredTechniques,
|
||||
getTotalRuleCount,
|
||||
populateSelected,
|
||||
} from './helpers';
|
||||
|
||||
|
@ -88,4 +89,26 @@ 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,10 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { EuiSelectableOption } from '@elastic/eui';
|
||||
import type {
|
||||
CoverageOverviewRuleActivity,
|
||||
CoverageOverviewRuleSource,
|
||||
} from '../../../../../common/api/detection_engine';
|
||||
import type { CoverageOverviewRuleSource } from '../../../../../common/api/detection_engine';
|
||||
import { CoverageOverviewRuleActivity } 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';
|
||||
|
@ -43,3 +41,20 @@ 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;
|
||||
};
|
||||
|
|
|
@ -97,7 +97,6 @@ const RuleActivityFilterComponent = ({
|
|||
<EuiPopoverTitle paddingSize="s">{i18n.CoverageOverviewFilterPopoverTitle}</EuiPopoverTitle>
|
||||
<EuiSelectable
|
||||
data-test-subj="coverageOverviewFilterList"
|
||||
isLoading={isLoading}
|
||||
options={options}
|
||||
onChange={handleSelectableOnChange}
|
||||
renderOption={renderOptionLabel}
|
||||
|
@ -120,7 +119,7 @@ const RuleActivityFilterComponent = ({
|
|||
iconType="cross"
|
||||
color="danger"
|
||||
size="xs"
|
||||
isDisabled={numActiveFilters === 0 || isLoading}
|
||||
isDisabled={numActiveFilters === 0}
|
||||
onClick={handleOnClear}
|
||||
>
|
||||
{i18n.CoverageOverviewFilterPopoverClearAll}
|
||||
|
|
|
@ -96,7 +96,6 @@ const RuleSourceFilterComponent = ({
|
|||
<EuiPopoverTitle paddingSize="s">{i18n.CoverageOverviewFilterPopoverTitle}</EuiPopoverTitle>
|
||||
<EuiSelectable
|
||||
data-test-subj="coverageOverviewFilterList"
|
||||
isLoading={isLoading}
|
||||
options={options}
|
||||
onChange={handleSelectableOnChange}
|
||||
renderOption={renderOptionLabel}
|
||||
|
@ -119,7 +118,7 @@ const RuleSourceFilterComponent = ({
|
|||
iconType="cross"
|
||||
color="danger"
|
||||
size="xs"
|
||||
isDisabled={numActiveFilters === 0 || isLoading}
|
||||
isDisabled={numActiveFilters === 0}
|
||||
onClick={handleOnClear}
|
||||
>
|
||||
{i18n.CoverageOverviewFilterPopoverClearAll}
|
||||
|
|
|
@ -10,7 +10,8 @@ import { css } from '@emotion/css';
|
|||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique';
|
||||
import { coverageOverviewPanelWidth } from './constants';
|
||||
import { getCardBackgroundColor } from './helpers';
|
||||
import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context';
|
||||
import { getCardBackgroundColor, getTotalRuleCount } from './helpers';
|
||||
import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -29,9 +30,13 @@ const CoverageOverviewMitreTechniquePanelComponent = ({
|
|||
isPopoverOpen,
|
||||
isExpanded,
|
||||
}: CoverageOverviewMitreTechniquePanelProps) => {
|
||||
const {
|
||||
state: { filter },
|
||||
} = useCoverageOverviewDashboardContext();
|
||||
const totalRuleCount = getTotalRuleCount(technique, filter.activity);
|
||||
const techniqueBackgroundColor = useMemo(
|
||||
() => getCardBackgroundColor(technique.enabledRules.length),
|
||||
[technique.enabledRules.length]
|
||||
() => getCardBackgroundColor(totalRuleCount),
|
||||
[totalRuleCount]
|
||||
);
|
||||
|
||||
const handlePanelOnClick = useCallback(
|
||||
|
|
|
@ -13,8 +13,10 @@ import { TestProviders } from '../../../../common/mock';
|
|||
import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique';
|
||||
import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover';
|
||||
import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context';
|
||||
import { useUserData } from '../../../../detections/components/user_info';
|
||||
|
||||
jest.mock('./coverage_overview_dashboard_context');
|
||||
jest.mock('../../../../detections/components/user_info');
|
||||
|
||||
const mockEnableAllDisabled = jest.fn();
|
||||
|
||||
|
@ -31,9 +33,10 @@ const renderTechniquePanelPopover = (
|
|||
describe('CoverageOverviewMitreTechniquePanelPopover', () => {
|
||||
beforeEach(() => {
|
||||
(useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue({
|
||||
state: { showExpandedCells: false },
|
||||
state: { showExpandedCells: false, filter: {} },
|
||||
actions: { enableAllDisabled: mockEnableAllDisabled },
|
||||
});
|
||||
(useUserData as jest.Mock).mockReturnValue([{ loading: false, canUserCRUD: true }]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -49,7 +52,7 @@ describe('CoverageOverviewMitreTechniquePanelPopover', () => {
|
|||
|
||||
test('it renders panel with expanded view', () => {
|
||||
(useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue({
|
||||
state: { showExpandedCells: true },
|
||||
state: { showExpandedCells: true, filter: {} },
|
||||
actions: { enableAllDisabled: mockEnableAllDisabled },
|
||||
});
|
||||
const wrapper = renderTechniquePanelPopover();
|
||||
|
@ -103,4 +106,14 @@ describe('CoverageOverviewMitreTechniquePanelPopover', () => {
|
|||
});
|
||||
expect(wrapper.getByTestId('enableAllDisabledButton')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('"Enable all disabled" button is disabled when user does not have CRUD permissions', async () => {
|
||||
(useUserData as jest.Mock).mockReturnValue([{ loading: false, canUserCRUD: false }]);
|
||||
const wrapper = renderTechniquePanelPopover();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(wrapper.getByTestId('coverageOverviewTechniquePanel'));
|
||||
});
|
||||
expect(wrapper.getByTestId('enableAllDisabledButton')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
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';
|
||||
|
@ -36,13 +37,19 @@ export interface CoverageOverviewMitreTechniquePanelPopoverProps {
|
|||
const CoverageOverviewMitreTechniquePanelPopoverComponent = ({
|
||||
technique,
|
||||
}: CoverageOverviewMitreTechniquePanelPopoverProps) => {
|
||||
const [{ loading: userInfoLoading, canUserCRUD }] = useUserData();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [isEnableButtonLoading, setIsDisableButtonLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const coveredSubtechniques = useMemo(() => getNumOfCoveredSubtechniques(technique), [technique]);
|
||||
const isEnableButtonDisabled = useMemo(
|
||||
() => technique.disabledRules.length === 0,
|
||||
[technique.disabledRules.length]
|
||||
() => !canUserCRUD || technique.disabledRules.length === 0,
|
||||
[canUserCRUD, technique.disabledRules.length]
|
||||
);
|
||||
|
||||
const isEnableButtonLoading = useMemo(
|
||||
() => isLoading || userInfoLoading,
|
||||
[isLoading, userInfoLoading]
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -51,10 +58,10 @@ const CoverageOverviewMitreTechniquePanelPopoverComponent = ({
|
|||
} = useCoverageOverviewDashboardContext();
|
||||
|
||||
const handleEnableAllDisabled = useCallback(async () => {
|
||||
setIsDisableButtonLoading(true);
|
||||
setIsLoading(true);
|
||||
const ruleIds = technique.disabledRules.map((rule) => rule.id);
|
||||
await enableAllDisabled(ruleIds);
|
||||
setIsDisableButtonLoading(false);
|
||||
setIsLoading(false);
|
||||
closePopover();
|
||||
}, [closePopover, enableAllDisabled, technique.disabledRules]);
|
||||
|
||||
|
|
|
@ -152,7 +152,7 @@ export const CoverageOverviewSearchBarPlaceholder = i18n.translate(
|
|||
'xpack.securitySolution.coverageOverviewDashboard.searchBarPlaceholder',
|
||||
{
|
||||
defaultMessage:
|
||||
'Search for the tactic, technique (e.g.,"defence evasion" or "TA0005") or rule name, index pattern (e.g.,"filebeat-*")',
|
||||
'Search for the tactic, technique (e.g.,"defense evasion" or "TA0005") or rule name',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -169,3 +169,18 @@ export const CoverageOverviewFilterPopoverClearAll = i18n.translate(
|
|||
defaultMessage: 'Clear all',
|
||||
}
|
||||
);
|
||||
|
||||
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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
COVERAGE_OVERVIEW_PATH,
|
||||
DATA_QUALITY_PATH,
|
||||
DETECTION_RESPONSE_PATH,
|
||||
ENTITY_ANALYTICS_PATH,
|
||||
|
@ -22,7 +21,6 @@ import {
|
|||
GETTING_STARTED,
|
||||
OVERVIEW,
|
||||
ENTITY_ANALYTICS,
|
||||
COVERAGE_OVERVIEW,
|
||||
} from '../app/translations';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
import overviewPageImg from '../common/images/overview_page.png';
|
||||
|
@ -113,24 +111,3 @@ export const ecsDataQualityDashboardLinks: LinkItem = {
|
|||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const coverageOverviewDashboardLinks: LinkItem = {
|
||||
id: SecurityPageName.coverageOverview,
|
||||
title: COVERAGE_OVERVIEW,
|
||||
landingImage: overviewPageImg, // TODO: change with updated image before removing feature flag https://github.com/elastic/security-team/issues/2905
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.appLinks.coverageOverviewDashboardDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'An overview of rule coverage according to the MITRE ATT&CK\u00AE specifications',
|
||||
}
|
||||
),
|
||||
path: COVERAGE_OVERVIEW_PATH,
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.coverageOverviewDashboard', {
|
||||
defaultMessage: 'MITRE ATT&CK Coverage',
|
||||
}),
|
||||
],
|
||||
experimentalKey: 'detectionsCoverageOverview',
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import {
|
||||
LANDING_PATH,
|
||||
OVERVIEW_PATH,
|
||||
|
@ -15,7 +14,6 @@ import {
|
|||
DETECTION_RESPONSE_PATH,
|
||||
SecurityPageName,
|
||||
ENTITY_ANALYTICS_PATH,
|
||||
COVERAGE_OVERVIEW_PATH,
|
||||
} from '../../common/constants';
|
||||
import type { SecuritySubPluginRoutes } from '../app/types';
|
||||
|
||||
|
@ -25,9 +23,7 @@ import { DetectionResponse } from './pages/detection_response';
|
|||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { EntityAnalyticsPage } from './pages/entity_analytics';
|
||||
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
|
||||
import { CoverageOverviewPage } from '../detection_engine/rule_management_ui/pages/coverage_overview';
|
||||
import { LandingPage } from './pages/landing';
|
||||
import { useIsExperimentalFeatureEnabled } from '../common/hooks/use_experimental_features';
|
||||
|
||||
const OverviewRoutes = () => (
|
||||
<PluginTemplateWrapper>
|
||||
|
@ -69,22 +65,6 @@ const DataQualityRoutes = () => (
|
|||
</PluginTemplateWrapper>
|
||||
);
|
||||
|
||||
const CoverageOverviewRoutes = () => {
|
||||
const isDetectionsCoverageOverviewEnabled = useIsExperimentalFeatureEnabled(
|
||||
'detectionsCoverageOverview'
|
||||
);
|
||||
|
||||
return isDetectionsCoverageOverviewEnabled ? (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.coverageOverview}>
|
||||
<CoverageOverviewPage />
|
||||
</TrackApplicationView>
|
||||
</PluginTemplateWrapper>
|
||||
) : (
|
||||
<Redirect to={SecurityPageName.landing} />
|
||||
);
|
||||
};
|
||||
|
||||
export const routes: SecuritySubPluginRoutes = [
|
||||
{
|
||||
path: OVERVIEW_PATH,
|
||||
|
@ -106,8 +86,4 @@ export const routes: SecuritySubPluginRoutes = [
|
|||
path: DATA_QUALITY_PATH,
|
||||
component: DataQualityRoutes,
|
||||
},
|
||||
{
|
||||
path: COVERAGE_OVERVIEW_PATH,
|
||||
component: CoverageOverviewRoutes,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -13,13 +13,22 @@ import {
|
|||
RULES_LANDING_PATH,
|
||||
RULES_ADD_PATH,
|
||||
SERVER_APP_ID,
|
||||
COVERAGE_OVERVIEW_PATH,
|
||||
} from '../../common/constants';
|
||||
import { ADD_RULES, CREATE_NEW_RULE, EXCEPTIONS, RULES, SIEM_RULES } from '../app/translations';
|
||||
import {
|
||||
ADD_RULES,
|
||||
COVERAGE_OVERVIEW,
|
||||
CREATE_NEW_RULE,
|
||||
EXCEPTIONS,
|
||||
RULES,
|
||||
SIEM_RULES,
|
||||
} from '../app/translations';
|
||||
import { SecurityPageName } from '../app/types';
|
||||
import { benchmarksLink } from '../cloud_security_posture/links';
|
||||
import type { LinkItem } from '../common/links';
|
||||
import { IconConsoleCloud } from '../common/icons/console_cloud';
|
||||
import { IconRollup } from '../common/icons/rollup';
|
||||
import { IconDashboards } from '../common/icons/dashboards';
|
||||
|
||||
export const links: LinkItem = {
|
||||
id: SecurityPageName.rulesLanding,
|
||||
|
@ -78,6 +87,25 @@ export const links: LinkItem = {
|
|||
],
|
||||
},
|
||||
benchmarksLink,
|
||||
{
|
||||
id: SecurityPageName.coverageOverview,
|
||||
title: COVERAGE_OVERVIEW,
|
||||
landingIcon: IconDashboards,
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.appLinks.coverageOverviewDashboardDescription',
|
||||
{
|
||||
defaultMessage: 'Review and maintain your protections MITRE ATT&CK® coverage',
|
||||
}
|
||||
),
|
||||
path: COVERAGE_OVERVIEW_PATH,
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.coverageOverviewDashboard', {
|
||||
defaultMessage: 'MITRE ATT&CK Coverage',
|
||||
}),
|
||||
],
|
||||
experimentalKey: 'detectionsCoverageOverview',
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
|
@ -90,5 +118,11 @@ export const links: LinkItem = {
|
|||
SecurityPageName.exceptions,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.discover', {
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
linkIds: [SecurityPageName.coverageOverview],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -10,7 +10,12 @@ import { Routes, Route } from '@kbn/shared-ux-router';
|
|||
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
import { RULES_LANDING_PATH, RULES_PATH, SecurityPageName } from '../../common/constants';
|
||||
import {
|
||||
COVERAGE_OVERVIEW_PATH,
|
||||
RULES_LANDING_PATH,
|
||||
RULES_PATH,
|
||||
SecurityPageName,
|
||||
} from '../../common/constants';
|
||||
import { NotFoundPage } from '../app/404';
|
||||
import { RulesPage } from '../detection_engine/rule_management_ui/pages/rule_management';
|
||||
import { CreateRulePage } from '../detection_engine/rule_creation_ui/pages/rule_creation';
|
||||
|
@ -26,6 +31,8 @@ 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 = [
|
||||
{
|
||||
|
@ -102,6 +109,22 @@ 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} />
|
||||
);
|
||||
};
|
||||
|
||||
export const routes: SecuritySubPluginRoutes = [
|
||||
{
|
||||
path: RULES_LANDING_PATH,
|
||||
|
@ -111,4 +134,8 @@ export const routes: SecuritySubPluginRoutes = [
|
|||
path: RULES_PATH,
|
||||
component: Rules,
|
||||
},
|
||||
{
|
||||
path: COVERAGE_OVERVIEW_PATH,
|
||||
component: CoverageOverviewRoutes,
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue