diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.test.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.test.ts new file mode 100644 index 000000000000..dffef3338072 --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.test.ts @@ -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) + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts index f4b1e6d41f2f..ba898093b865 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts @@ -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 diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx index 470a04e40248..3812bb1b88d1 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx @@ -51,6 +51,29 @@ describe('AccountsEvaluatedWidget', () => { ); }); + it('calls navToFindingsByCloudProvider when a benchmark with provider and namespace is clicked', () => { + const { getByText } = render( + + + + ); + + 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( @@ -67,4 +90,26 @@ describe('AccountsEvaluatedWidget', () => { ['orchestrator.cluster.id'] ); }); + + it('calls navToFindingsByCisBenchmark when a benchmark with benchmarkId and namespace is clicked', () => { + const { getByText } = render( + + + + ); + + fireEvent.click(getByText('20')); + + expect(mockNavToFindings).toHaveBeenCalledWith( + { + 'rule.benchmark.id': 'cis_k8s', + 'data_stream.namespace': 'test-namespace', + }, + ['orchestrator.cluster.id'] + ); + }); }); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 80716efd9869..4b8208ed4b0d 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -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) => { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 02a4aaf4f12e..50f9c30c2fab 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -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 ( <> - + - + ); @@ -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} /> @@ -319,6 +330,7 @@ const TabContent = ({ complianceData={getDashboardData.data} notInstalledConfig={getNotInstalledConfig(policyTemplate, integrationLink)} isIntegrationInstalled={setupStatus !== 'not-installed'} + activeNamespace={activeNamespace} /> @@ -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: ( ), }, @@ -439,7 +451,7 @@ export const ComplianceDashboard = () => { content: ( ), }, @@ -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 diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.test.tsx new file mode 100644 index 000000000000..d22a527c691a --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.test.tsx @@ -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( + + + + ); + + 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'] + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx index 869b1473b443..203c63d3be2a 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx @@ -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 = { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx index 486624caa0c1..ce439ed503c6 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx @@ -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('', () => { - const renderBenchmarks = (alterMockData = {}) => + const renderBenchmarks = (alterMockData = {}, namespace?: string) => render( ); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx index f4cc7a5ba002..7254b7b6bdc0 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx @@ -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 = ({ `} > - + = { 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: , + title: ( + + ), button: ( { - 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(