Skip to content

Commit

Permalink
[Cloud Security] Findings flyout links to rule page with open flyout (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanSh committed Feb 6, 2024
1 parent d2f6119 commit 5c8efc2
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export interface BenchmarkRuleSelectParams {
export interface PageUrlParams {
benchmarkId: BenchmarksCisId;
benchmarkVersion: string;
ruleId?: string;
}

export const rulesToUpdate = schema.arrayOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const cloudPosturePages: Record<CspPage, CspPageNavigationItem> = {
export const benchmarksNavigation: Record<CspBenchmarksPage, CspPageNavigationItem> = {
rules: {
name: NAV_ITEMS_NAMES.RULES,
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/benchmarks/:benchmarkId/:benchmarkVersion/rules`,
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/benchmarks/:benchmarkId/:benchmarkVersion/rules/:ruleId?`,
id: 'cloud_security_posture-benchmarks-rules',
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ export const ColumnNameWithTooltip = ({
tooltipContent: EuiToolTipProps['content'];
columnName: ReactNode;
}) => (
<EuiToolTip content={tooltipContent}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem>
<span>{columnName}</span>
</EuiFlexItem>
<EuiFlexItem>
<EuiIcon size="m" color="subdued" type="questionInCircle" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem>
<span>{columnName}</span>
</EuiFlexItem>
<EuiFlexItem>
<EuiToolTip content={tooltipContent}>
<EuiIcon size="m" color="subdued" type="iInCircle" />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ interface SeverityStatusBadgeProps {

export const CVSScoreBadge = ({ score, version }: CVSScoreBadgeProps) => {
if (!score) return null;

const color = getCvsScoreColor(score);
const versionDisplay = version ? `v${version.split('.')[0]}` : null;

return (
<EuiBadge
color={color}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
import { assertNever } from '@kbn/std';
import { i18n } from '@kbn/i18n';
import type { HttpSetup } from '@kbn/core/public';
import { generatePath } from 'react-router-dom';
import { benchmarksNavigation } from '../../../common/navigation/constants';
import cisLogoIcon from '../../../assets/icons/cis_logo.svg';
import { CspFinding } from '../../../../common/schemas/csp_finding';
import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge';
Expand All @@ -40,6 +42,7 @@ import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon';
import { BenchmarkName } from '../../../../common/types_old';
import { FINDINGS_FLYOUT } from '../test_subjects';
import { createDetectionRuleFromFinding } from '../utils/create_detection_rule_from_finding';
import { useKibana } from '../../../common/hooks/use_kibana';

const tabs = [
{
Expand Down Expand Up @@ -110,11 +113,21 @@ export const CisKubernetesIcons = ({
);

const FindingsTab = ({ tab, findings }: { findings: CspFinding; tab: FindingsTab }) => {
const { application } = useKibana().services;

const ruleFlyoutLink = application.getUrlForApp('security', {
path: generatePath(benchmarksNavigation.rules.path, {
benchmarkVersion: findings.rule.benchmark.version.split('v')[1], // removing the v from the version
benchmarkId: findings.rule.benchmark.id,
ruleId: findings.rule.id,
}),
});

switch (tab.id) {
case 'overview':
return <OverviewTab data={findings} />;
return <OverviewTab data={findings} ruleFlyoutLink={ruleFlyoutLink} />;
case 'rule':
return <RuleTab data={findings} />;
return <RuleTab data={findings} ruleFlyoutLink={ruleFlyoutLink} />;
case 'table':
return <TableTab data={findings} />;
case 'json':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EuiPanel,
EuiSpacer,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import moment from 'moment';
Expand All @@ -36,12 +37,21 @@ import { FindingsDetectionRuleCounter } from './findings_detection_rule_counter'
type Accordion = Pick<EuiAccordionProps, 'title' | 'id' | 'initialIsOpen'> &
Pick<EuiDescriptionListProps, 'listItems'>;

const getDetailsList = (data: CspFinding, discoverIndexLink: string | undefined) => [
const getDetailsList = (data: CspFinding, ruleFlyoutLink: string, discoverIndexLink?: string) => [
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleNameTitle', {
defaultMessage: 'Rule Name',
}),
description: data.rule.name,
description: (
<EuiToolTip
position="top"
content={i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleNameTooltip', {
defaultMessage: 'Manage Rule',
})}
>
<EuiLink href={ruleFlyoutLink}>{data.rule.name}</EuiLink>
</EuiToolTip>
),
},
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle', {
Expand Down Expand Up @@ -160,10 +170,14 @@ const getEvidenceList = ({ result }: CspFinding) =>
},
].filter(truthy);

export const OverviewTab = ({ data }: { data: CspFinding }) => {
const {
services: { discover },
} = useKibana();
export const OverviewTab = ({
data,
ruleFlyoutLink,
}: {
data: CspFinding;
ruleFlyoutLink: string;
}) => {
const { discover } = useKibana().services;
const latestFindingsDataView = useLatestFindingsDataView(LATEST_FINDINGS_INDEX_PATTERN);

const discoverIndexLink = useMemo(
Expand All @@ -185,7 +199,7 @@ export const OverviewTab = ({ data }: { data: CspFinding }) => {
defaultMessage: 'Details',
}),
id: 'detailsAccordion',
listItems: getDetailsList(data, discoverIndexLink),
listItems: getDetailsList(data, ruleFlyoutLink, discoverIndexLink),
},
{
initialIsOpen: true,
Expand All @@ -206,7 +220,7 @@ export const OverviewTab = ({ data }: { data: CspFinding }) => {
listItems: getEvidenceList(data),
},
].filter(truthy),
[data, discoverIndexLink, hasEvidence]
[data, discoverIndexLink, hasEvidence, ruleFlyoutLink]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@
* 2.0.
*/

import { EuiBadge, EuiDescriptionList } from '@elastic/eui';
import { EuiBadge, EuiDescriptionList, EuiLink, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { CspFinding } from '../../../../common/schemas/csp_finding';
import { CisKubernetesIcons, CspFlyoutMarkdown } from './findings_flyout';

export const getRuleList = (rule: CspFinding['rule']) => [
export const getRuleList = (rule: CspFinding['rule'], ruleFlyoutLink?: string) => [
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.nameTitle', {
defaultMessage: 'Name',
}),
description: rule.name,
description: ruleFlyoutLink ? (
<EuiToolTip
position="top"
content={i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.nameTooltip', {
defaultMessage: 'Manage Rule',
})}
>
<EuiLink href={ruleFlyoutLink}>{rule.name}</EuiLink>
</EuiToolTip>
) : (
rule.name
),
},
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.descriptionTitle', {
Expand Down Expand Up @@ -80,6 +91,6 @@ export const getRuleList = (rule: CspFinding['rule']) => [
: []),
];

export const RuleTab = ({ data }: { data: CspFinding }) => (
<EuiDescriptionList listItems={getRuleList(data.rule)} />
);
export const RuleTab = ({ data, ruleFlyoutLink }: { data: CspFinding; ruleFlyoutLink: string }) => {
return <EuiDescriptionList listItems={getRuleList(data.rule, ruleFlyoutLink)} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import compareVersions from 'compare-versions';
import { EuiSpacer } from '@elastic/eui';
import { useParams } from 'react-router-dom';
import { useParams, useHistory, generatePath } from 'react-router-dom';
import { benchmarksNavigation } from '../../common/navigation/constants';
import { buildRuleKey } from '../../../common/utils/rules_states';
import { extractErrorMessage } from '../../../common/utils/helpers';
import { RulesTable } from './rules_table';
Expand Down Expand Up @@ -41,7 +42,10 @@ interface RulesPageData {

export type RulesState = RulesPageData & RulesQuery;

const getRulesPage = (
const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) =>
data.slice(page * perPage, (page + 1) * perPage);

const getRulesPageData = (
data: CspBenchmarkRulesWithStates[],
status: string,
error: unknown,
Expand All @@ -59,17 +63,45 @@ const getRulesPage = (
};
};

const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) =>
data.slice(page * perPage, (page + 1) * perPage);

const MAX_ITEMS_PER_PAGE = 10000;

export const RulesContainer = () => {
const params = useParams<PageUrlParams>();
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
const history = useHistory();
const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter');
const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY);

const navToRuleFlyout = (ruleId: string) => {
history.push(
generatePath(benchmarksNavigation.rules.path, {
benchmarkVersion: params.benchmarkVersion,
benchmarkId: params.benchmarkId,
ruleId,
})
);
};

const navToRulePage = () => {
history.push(
generatePath(benchmarksNavigation.rules.path, {
benchmarkVersion: params.benchmarkVersion,
benchmarkId: params.benchmarkId,
})
);
};

// We need to make this call without filters. this way the section list is always full
const allRules = useFindCspBenchmarkRule(
{
page: 1,
perPage: MAX_ITEMS_PER_PAGE,
sortField: 'metadata.benchmark.rule_number',
sortOrder: 'asc',
},
params.benchmarkId,
params.benchmarkVersion
);

const [rulesQuery, setRulesQuery] = useState<RulesQuery>({
section: undefined,
ruleNumber: undefined,
Expand All @@ -80,6 +112,30 @@ export const RulesContainer = () => {
sortOrder: 'asc',
});

// This useEffect is in charge of auto paginating to the correct page of a rule from the url params
useEffect(() => {
const getPageByRuleId = () => {
if (params.ruleId && allRules.data?.items) {
const ruleIndex = allRules.data.items.findIndex(
(rule) => rule.metadata.id === params.ruleId
);

if (ruleIndex !== -1) {
// Calculate the page based on the rule index and page size
const rulePage = Math.floor(ruleIndex / pageSize);
return rulePage;
}
}
return 0;
};

setRulesQuery({
...rulesQuery,
page: getPageByRuleId(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allRules.data?.items]);

const { data, status, error } = useFindCspBenchmarkRule(
{
section: rulesQuery.section,
Expand All @@ -94,27 +150,9 @@ export const RulesContainer = () => {
params.benchmarkVersion
);

// We need to make this call again without the filters. this way the section list is always full
const allRules = useFindCspBenchmarkRule(
{
page: 1,
perPage: MAX_ITEMS_PER_PAGE,
sortField: 'metadata.benchmark.rule_number',
sortOrder: 'asc',
},
params.benchmarkId,
params.benchmarkVersion
);

const rulesStates = useCspGetRulesStates();
const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {});

const filteredRulesStates: RuleStateAttributes[] = arrayRulesStates.filter(
(ruleState: RuleStateAttributes) =>
ruleState.benchmark_id === params.benchmarkId &&
ruleState.benchmark_version === 'v' + params.benchmarkVersion
);

const rulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => {
if (!data) return [];

Expand Down Expand Up @@ -162,7 +200,7 @@ export const RulesContainer = () => {
const cleanedRuleNumberList = [...new Set(ruleNumberList)].sort(compareVersions);

const rulesPageData = useMemo(
() => getRulesPage(filteredRulesWithStates, status, error, rulesQuery),
() => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery),
[filteredRulesWithStates, status, error, rulesQuery]
);

Expand All @@ -175,13 +213,14 @@ export const RulesContainer = () => {
const rulesFlyoutData: CspBenchmarkRulesWithStates = {
...{
state:
filteredRulesStates.find(
(filteredRuleState) => filteredRuleState.rule_id === selectedRuleId
)?.muted === true
arrayRulesStates.find((filteredRuleState) => filteredRuleState.rule_id === params.ruleId)
?.muted === true
? 'muted'
: 'unmuted',
},
...{ metadata: rulesPageData.rules_map.get(selectedRuleId!)?.metadata! },
...{
metadata: allRules.data?.items.find((rule) => rule.metadata.id === params.ruleId)?.metadata!,
},
};

return (
Expand Down Expand Up @@ -227,16 +266,16 @@ export const RulesContainer = () => {
setPageSize(paginationQuery.perPage);
setRulesQuery((currentQuery) => ({ ...currentQuery, ...paginationQuery }));
}}
setSelectedRuleId={setSelectedRuleId}
selectedRuleId={selectedRuleId}
selectedRuleId={params.ruleId}
onRuleClick={navToRuleFlyout}
refetchRulesStates={rulesStates.refetch}
selectedRules={selectedRules}
setSelectedRules={setSelectedRules}
/>
{selectedRuleId && (
{params.ruleId && rulesFlyoutData.metadata && (
<RuleFlyout
rule={rulesFlyoutData}
onClose={() => setSelectedRuleId(null)}
onClose={navToRulePage}
refetchRulesStates={rulesStates.refetch}
/>
)}
Expand Down
Loading

0 comments on commit 5c8efc2

Please sign in to comment.