Skip to content

Commit

Permalink
[Security Solution] Coverage Overview follow-up (elastic#164613)
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee authored Aug 25, 2023
1 parent b5b2c36 commit 168e3dc
Show file tree
Hide file tree
Showing 16 changed files with 181 additions and 78 deletions.
2 changes: 0 additions & 2 deletions x-pack/plugins/security_solution/public/dashboards/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
detectionResponseLinks,
entityAnalyticsLinks,
overviewLinks,
coverageOverviewDashboardLinks,
} from '../overview/links';
import { IconDashboards } from '../common/icons/dashboards';

Expand All @@ -27,7 +26,6 @@ const subLinks: LinkItem[] = [
vulnerabilityDashboardLink,
entityAnalyticsLinks,
ecsDataQualityDashboardLinks,
coverageOverviewDashboardLinks,
].map((link) => ({ ...link, sideNavIcon: IconDashboards }));

export const dashboardsLinks: LinkItem = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,6 +17,7 @@ import {
extractSelected,
getNumOfCoveredSubtechniques,
getNumOfCoveredTechniques,
getTotalRuleCount,
populateSelected,
} from './helpers';

Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -120,7 +119,7 @@ const RuleActivityFilterComponent = ({
iconType="cross"
color="danger"
size="xs"
isDisabled={numActiveFilters === 0 || isLoading}
isDisabled={numActiveFilters === 0}
onClick={handleOnClear}
>
{i18n.CoverageOverviewFilterPopoverClearAll}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -119,7 +118,7 @@ const RuleSourceFilterComponent = ({
iconType="cross"
color="danger"
size="xs"
isDisabled={numActiveFilters === 0 || isLoading}
isDisabled={numActiveFilters === 0}
onClick={handleOnClear}
>
{i18n.CoverageOverviewFilterPopoverClearAll}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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(() => {
Expand All @@ -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();
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);

Expand All @@ -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.',
}
);
23 changes: 0 additions & 23 deletions x-pack/plugins/security_solution/public/overview/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import { i18n } from '@kbn/i18n';
import {
COVERAGE_OVERVIEW_PATH,
DATA_QUALITY_PATH,
DETECTION_RESPONSE_PATH,
ENTITY_ANALYTICS_PATH,
Expand All @@ -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';
Expand Down Expand Up @@ -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',
};
Loading

0 comments on commit 168e3dc

Please sign in to comment.