[Cloud Security] [Bug] Namespace filtering Findings from CSPM dashboard links (#225161)

This commit is contained in:
seanrathier 2025-06-27 00:20:15 -04:00 committed by GitHub
parent 8397de18ef
commit 35b2a22f02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 270 additions and 44 deletions

View file

@ -0,0 +1,59 @@
/*
* 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 { renderHook, act } from '@testing-library/react';
import { useActiveNamespace } from './use_active_namespace';
import { LOCAL_STORAGE_NAMESPACE_KEY } from '../constants';
describe('useActiveNamespace', () => {
it('should return the active namespace from local storage for CSPM', () => {
const { result } = renderHook(() => useActiveNamespace({ postureType: 'cspm' }));
expect(result.current).toEqual({
activeNamespace: 'default',
updateActiveNamespace: expect.any(Function),
});
});
it('should update the active namespace and local storage when updateActiveNamespace is called with CSPM', async () => {
const postureType = 'cspm';
const CSPM_NAMESPACE_LOCAL_STORAGE_KEY = `${LOCAL_STORAGE_NAMESPACE_KEY}:${postureType}`;
const { result } = renderHook(() => useActiveNamespace({ postureType }));
const newNamespace = 'test-namespace';
act(() => {
result.current.updateActiveNamespace(newNamespace);
});
expect(result.current.activeNamespace).toBe(newNamespace);
expect(localStorage.getItem(CSPM_NAMESPACE_LOCAL_STORAGE_KEY)).toBe(
JSON.stringify(newNamespace)
);
});
it('should return the active namespace from local storage for KSPM', () => {
const { result } = renderHook(() => useActiveNamespace({ postureType: 'kspm' }));
expect(result.current).toEqual({
activeNamespace: 'default',
updateActiveNamespace: expect.any(Function),
});
});
it('should update the active namespace and local storage when updateActiveNamespace is called with KSPM', async () => {
const postureType = 'kspm';
const KSPM_NAMESPACE_LOCAL_STORAGE_KEY = `${LOCAL_STORAGE_NAMESPACE_KEY}:${postureType}`;
const { result } = renderHook(() => useActiveNamespace({ postureType }));
const newNamespace = 'test-namespace';
act(() => {
result.current.updateActiveNamespace(newNamespace);
});
expect(result.current.activeNamespace).toBe(newNamespace);
expect(localStorage.getItem(KSPM_NAMESPACE_LOCAL_STORAGE_KEY)).toBe(
JSON.stringify(newNamespace)
);
});
});

View file

@ -9,7 +9,7 @@ import { useState, useCallback } from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { LOCAL_STORAGE_NAMESPACE_KEY, DEFAULT_NAMESPACE } from '../constants';
export const useActiveNamespace = ({ postureType }: { postureType?: 'cspm' | 'kspm' }) => {
export const useActiveNamespace = ({ postureType }: { postureType: 'cspm' | 'kspm' }) => {
const [localStorageActiveNamespace, localStorageSetActiveNamespace] = useLocalStorage(
`${LOCAL_STORAGE_NAMESPACE_KEY}:${postureType}`,
DEFAULT_NAMESPACE

View file

@ -51,6 +51,29 @@ describe('AccountsEvaluatedWidget', () => {
);
});
it('calls navToFindingsByCloudProvider when a benchmark with provider and namespace is clicked', () => {
const { getByText } = render(
<TestProvider>
<AccountsEvaluatedWidget
activeNamespace="test-namespace"
benchmarkAssets={benchmarkAssets}
benchmarkAbbreviateAbove={999}
/>
</TestProvider>
);
fireEvent.click(getByText('10'));
expect(mockNavToFindings).toHaveBeenCalledWith(
{
'data_stream.namespace': 'test-namespace',
'cloud.provider': 'aws',
'rule.benchmark.posture_type': 'cspm',
},
['cloud.account.id']
);
});
it('calls navToFindingsByCisBenchmark when a benchmark with benchmarkId is clicked', () => {
const { getByText } = render(
<TestProvider>
@ -67,4 +90,26 @@ describe('AccountsEvaluatedWidget', () => {
['orchestrator.cluster.id']
);
});
it('calls navToFindingsByCisBenchmark when a benchmark with benchmarkId and namespace is clicked', () => {
const { getByText } = render(
<TestProvider>
<AccountsEvaluatedWidget
benchmarkAssets={benchmarkAssets}
benchmarkAbbreviateAbove={999}
activeNamespace="test-namespace"
/>
</TestProvider>
);
fireEvent.click(getByText('20'));
expect(mockNavToFindings).toHaveBeenCalledWith(
{
'rule.benchmark.id': 'cis_k8s',
'data_stream.namespace': 'test-namespace',
},
['orchestrator.cluster.id']
);
});
});

View file

@ -46,9 +46,11 @@ const benchmarks = [
];
export const AccountsEvaluatedWidget = ({
activeNamespace,
benchmarkAssets,
benchmarkAbbreviateAbove = 999,
}: {
activeNamespace?: string;
benchmarkAssets: BenchmarkData[];
/** numbers higher than the value of this field will be abbreviated using compact notation and have a tooltip displaying the full value */
benchmarkAbbreviateAbove?: number;
@ -63,15 +65,27 @@ export const AccountsEvaluatedWidget = ({
const navToFindingsByCloudProvider = (provider: string) => {
navToFindings(
{ 'cloud.provider': provider, 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE },
activeNamespace
? {
[`${FINDINGS_GROUPING_OPTIONS.NAMESPACE}`]: activeNamespace,
'cloud.provider': provider,
'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE,
}
: { 'cloud.provider': provider, 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE },
[FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID]
);
};
const navToFindingsByCisBenchmark = (cisBenchmark: string) => {
navToFindings({ 'rule.benchmark.id': cisBenchmark }, [
FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID,
]);
navToFindings(
activeNamespace
? {
[`${FINDINGS_GROUPING_OPTIONS.NAMESPACE}`]: activeNamespace,
'rule.benchmark.id': cisBenchmark,
}
: { 'rule.benchmark.id': cisBenchmark },
[FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID]
);
};
const benchmarkElements = benchmarks.map((benchmark) => {

View file

@ -131,11 +131,13 @@ const IntegrationPostureDashboard = ({
notInstalledConfig,
isIntegrationInstalled,
dashboardType,
activeNamespace,
}: {
complianceData: ComplianceDashboardDataV2 | undefined;
notInstalledConfig: CspNoDataPageProps;
isIntegrationInstalled?: boolean;
dashboardType: PosturePolicyTemplate;
activeNamespace?: string;
}) => {
const noFindings = !complianceData || complianceData.stats.totalFindings === 0;
@ -183,9 +185,17 @@ const IntegrationPostureDashboard = ({
// there are findings, displays dashboard even if integration is not installed
return (
<>
<SummarySection complianceData={complianceData!} dashboardType={dashboardType} />
<SummarySection
complianceData={complianceData!}
dashboardType={dashboardType}
activeNamespace={activeNamespace}
/>
<EuiSpacer />
<BenchmarksSection complianceData={complianceData!} dashboardType={dashboardType} />
<BenchmarksSection
complianceData={complianceData!}
dashboardType={dashboardType}
activeNamespace={activeNamespace}
/>
<EuiSpacer />
</>
);
@ -244,7 +254,7 @@ const TabContent = ({
activeNamespace,
}: {
selectedPostureTypeTab: PosturePolicyTemplate;
activeNamespace: string;
activeNamespace?: string;
}) => {
const { data: getSetupStatus } = useCspSetupStatusApi({
refetchInterval: (data) => {
@ -310,6 +320,7 @@ const TabContent = ({
complianceData={getDashboardData.data}
notInstalledConfig={getNotInstalledConfig(policyTemplate, integrationLink)}
isIntegrationInstalled={setupStatus !== 'not-installed'}
activeNamespace={activeNamespace}
/>
</Route>
@ -319,6 +330,7 @@ const TabContent = ({
complianceData={getDashboardData.data}
notInstalledConfig={getNotInstalledConfig(policyTemplate, integrationLink)}
isIntegrationInstalled={setupStatus !== 'not-installed'}
activeNamespace={activeNamespace}
/>
</Route>
</Routes>
@ -353,7 +365,7 @@ export const ComplianceDashboard = () => {
}, [location.pathname]);
const { activeNamespace, updateActiveNamespace } = useActiveNamespace({
postureType: currentTabUrlState,
postureType: currentTabUrlState || POSTURE_TYPE_CSPM,
});
const getCspmDashboardData = useCspmStatsApi(
@ -423,7 +435,7 @@ export const ComplianceDashboard = () => {
content: (
<TabContent
selectedPostureTypeTab={selectedTab || POSTURE_TYPE_CSPM}
activeNamespace={activeNamespace}
activeNamespace={cloudSecurityNamespaceSupportEnabled ? activeNamespace : undefined}
/>
),
},
@ -439,7 +451,7 @@ export const ComplianceDashboard = () => {
content: (
<TabContent
selectedPostureTypeTab={selectedTab || POSTURE_TYPE_KSPM}
activeNamespace={activeNamespace}
activeNamespace={cloudSecurityNamespaceSupportEnabled ? activeNamespace : undefined}
/>
),
},
@ -453,6 +465,7 @@ export const ComplianceDashboard = () => {
history,
services.data.query.queryString,
services.data.query.filterManager,
cloudSecurityNamespaceSupportEnabled,
]);
// if there is more than one namespace, show the namespace selector in the header

View file

@ -0,0 +1,46 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { TestProvider } from '../../../test/test_provider';
import { BenchmarkDetailsBox } from './benchmark_details_box';
import { getBenchmarkMockData } from '../mock';
const mockNavToFindings = jest.fn();
jest.mock('@kbn/cloud-security-posture/src/hooks/use_navigate_findings', () => ({
useNavigateFindings: () => mockNavToFindings,
}));
describe('BenchmarkDetailsBox', () => {
const renderBenchmarkDetails = () =>
render(
<TestProvider>
<BenchmarkDetailsBox benchmark={getBenchmarkMockData()} activeNamespace="test-namespace" />
</TestProvider>
);
it('renders the component correctly', () => {
const { getByTestId } = renderBenchmarkDetails();
expect(getByTestId('benchmark-asset-type')).toBeInTheDocument();
});
it('calls the navigate function with correct parameters when a benchmark is clicked', () => {
const { getByTestId } = renderBenchmarkDetails();
const benchmarkLink = getByTestId('benchmark-asset-type');
benchmarkLink.click();
expect(mockNavToFindings).toHaveBeenCalledWith(
{
'data_stream.namespace': 'test-namespace',
'rule.benchmark.id': 'cis_aws',
'rule.benchmark.version': '1.2.3',
},
['cloud.account.id']
);
});
});

View file

@ -23,23 +23,32 @@ import { getBenchmarkIdQuery } from './benchmarks_section';
import { BenchmarkData } from '../../../../common/types_old';
import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon';
import cisLogoIcon from '../../../assets/icons/cis_logo.svg';
interface BenchmarkInfo {
name: string;
assetType: string;
handleClick: () => void;
}
export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) => {
export const BenchmarkDetailsBox = ({
benchmark,
activeNamespace,
}: {
benchmark: BenchmarkData;
activeNamespace?: string;
}) => {
const navToFindings = useNavigateFindings();
const handleClickCloudProvider = () =>
navToFindings(getBenchmarkIdQuery(benchmark), [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID]);
const handleClickCloudProvider = () => {
navToFindings(getBenchmarkIdQuery(benchmark, activeNamespace), [
FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID,
]);
};
const handleClickCluster = () =>
navToFindings(getBenchmarkIdQuery(benchmark), [
const handleClickCluster = () => {
navToFindings(getBenchmarkIdQuery(benchmark, activeNamespace), [
FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID,
]);
};
const getBenchmarkInfo = (benchmarkId: string, cloudAssetCount: number): BenchmarkInfo => {
const benchmarks: Record<string, BenchmarkInfo> = {

View file

@ -17,13 +17,19 @@ import {
DASHBOARD_TABLE_HEADER_SCORE_TEST_ID,
} from '../test_subjects';
const mockNavToFindings = jest.fn();
jest.mock('@kbn/cloud-security-posture/src/hooks/use_navigate_findings', () => ({
useNavigateFindings: () => mockNavToFindings,
}));
describe('<BenchmarksSection />', () => {
const renderBenchmarks = (alterMockData = {}) =>
const renderBenchmarks = (alterMockData = {}, namespace?: string) =>
render(
<TestProvider>
<BenchmarksSection
complianceData={{ ...getMockDashboardData(), ...alterMockData }}
dashboardType={KSPM_POLICY_TEMPLATE}
activeNamespace={namespace}
/>
</TestProvider>
);

View file

@ -35,19 +35,30 @@ import { ComplianceScoreChart } from '../compliance_charts/compliance_score_char
import { BenchmarkDetailsBox } from './benchmark_details_box';
const BENCHMARK_DEFAULT_SORT_ORDER = 'asc';
export const getBenchmarkIdQuery = (benchmark: BenchmarkData): NavFilter => {
return {
'rule.benchmark.id': benchmark.meta.benchmarkId,
'rule.benchmark.version': benchmark.meta.benchmarkVersion,
};
export const getBenchmarkIdQuery = (
benchmark: BenchmarkData,
activeNamespace?: string
): NavFilter => {
return activeNamespace
? {
'rule.benchmark.id': benchmark.meta.benchmarkId,
'rule.benchmark.version': benchmark.meta.benchmarkVersion,
[`${FINDINGS_GROUPING_OPTIONS.NAMESPACE}`]: activeNamespace,
}
: {
'rule.benchmark.id': benchmark.meta.benchmarkId,
'rule.benchmark.version': benchmark.meta.benchmarkVersion,
};
};
export const BenchmarksSection = ({
complianceData,
dashboardType,
activeNamespace,
}: {
complianceData: ComplianceDashboardDataV2;
dashboardType: PosturePolicyTemplate;
activeNamespace?: string;
}) => {
const { euiTheme } = useEuiTheme();
const navToFindings = useNavigateFindings();
@ -65,32 +76,30 @@ export const BenchmarksSection = ({
benchmark: BenchmarkData,
evaluation: Evaluation,
groupBy: string[] = [FINDINGS_GROUPING_OPTIONS.NONE]
) => {
) =>
navToFindings(
{
...getPolicyTemplateQuery(dashboardType),
...getBenchmarkIdQuery(benchmark),
...getPolicyTemplateQuery(dashboardType, activeNamespace),
...getBenchmarkIdQuery(benchmark, activeNamespace),
'result.evaluation': evaluation,
},
groupBy
);
};
const navToFailedFindingsByBenchmarkAndSection = (
benchmark: BenchmarkData,
ruleSection: string,
resultEvaluation: 'passed' | 'failed' = RULE_FAILED
) => {
) =>
navToFindings(
{
...getPolicyTemplateQuery(dashboardType),
...getBenchmarkIdQuery(benchmark),
...getPolicyTemplateQuery(dashboardType, activeNamespace),
...getBenchmarkIdQuery(benchmark, activeNamespace),
'rule.section': ruleSection,
'result.evaluation': resultEvaluation,
},
[FINDINGS_GROUPING_OPTIONS.NONE]
);
};
const navToFailedFindingsByBenchmark = (benchmark: BenchmarkData) => {
navToFindingsByBenchmarkAndEvaluation(benchmark, RULE_FAILED, [
@ -175,7 +184,7 @@ export const BenchmarksSection = ({
`}
>
<EuiFlexItem grow={dashboardColumnsGrow.first}>
<BenchmarkDetailsBox benchmark={benchmark} />
<BenchmarkDetailsBox benchmark={benchmark} activeNamespace={activeNamespace} />
</EuiFlexItem>
<EuiFlexItem
grow={dashboardColumnsGrow.second}

View file

@ -40,16 +40,27 @@ export const dashboardColumnsGrow: Record<string, EuiFlexItemProps['grow']> = {
third: 8,
};
export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate): NavFilter => ({
'rule.benchmark.posture_type': policyTemplate,
});
export const getPolicyTemplateQuery = (
policyTemplate: PosturePolicyTemplate,
activeNamespace?: string
): NavFilter =>
activeNamespace
? {
'rule.benchmark.posture_type': policyTemplate,
[`${FINDINGS_GROUPING_OPTIONS.NAMESPACE}`]: activeNamespace,
}
: {
'rule.benchmark.posture_type': policyTemplate,
};
export const SummarySection = ({
dashboardType,
complianceData,
activeNamespace,
}: {
dashboardType: PosturePolicyTemplate;
complianceData: ComplianceDashboardDataV2;
activeNamespace?: string;
}) => {
const navToFindings = useNavigateFindings();
const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE);
@ -58,9 +69,13 @@ export const SummarySection = ({
const { euiTheme } = useEuiTheme();
const handleEvalCounterClick = (evaluation: Evaluation) => {
navToFindings({ 'result.evaluation': evaluation, ...getPolicyTemplateQuery(dashboardType) }, [
FINDINGS_GROUPING_OPTIONS.NONE,
]);
navToFindings(
{
'result.evaluation': evaluation,
...getPolicyTemplateQuery(dashboardType, activeNamespace),
},
[FINDINGS_GROUPING_OPTIONS.NONE]
);
};
const handleCellClick = (
@ -69,7 +84,7 @@ export const SummarySection = ({
) => {
navToFindings(
{
...getPolicyTemplateQuery(dashboardType),
...getPolicyTemplateQuery(dashboardType, activeNamespace),
'rule.section': ruleSection,
'result.evaluation': resultEvaluation,
},
@ -78,9 +93,13 @@ export const SummarySection = ({
};
const handleViewAllClick = () => {
navToFindings({ 'result.evaluation': RULE_FAILED, ...getPolicyTemplateQuery(dashboardType) }, [
FINDINGS_GROUPING_OPTIONS.RULE_SECTION,
]);
navToFindings(
{
'result.evaluation': RULE_FAILED,
...getPolicyTemplateQuery(dashboardType, activeNamespace),
},
[FINDINGS_GROUPING_OPTIONS.RULE_SECTION]
);
};
const counters: CspCounterCardProps[] = useMemo(
@ -97,7 +116,12 @@ export const SummarySection = ({
'xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription',
{ defaultMessage: 'Accounts Evaluated' }
),
title: <AccountsEvaluatedWidget benchmarkAssets={complianceData.benchmarks} />,
title: (
<AccountsEvaluatedWidget
activeNamespace={activeNamespace}
benchmarkAssets={complianceData.benchmarks}
/>
),
button: (
<EuiButtonEmpty
iconType="listAdd"
@ -130,7 +154,7 @@ export const SummarySection = ({
iconType="search"
data-test-subj="dashboard-view-all-resources"
onClick={() => {
navToFindings(getPolicyTemplateQuery(dashboardType), [
navToFindings(getPolicyTemplateQuery(dashboardType, activeNamespace), [
FINDINGS_GROUPING_OPTIONS.RESOURCE_ID,
]);
}}
@ -150,6 +174,7 @@ export const SummarySection = ({
dashboardType,
kspmIntegrationLink,
navToFindings,
activeNamespace,
]
);
const chartTitle = i18n.translate(