diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index bfce821222e29..aa25c70eb247d 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -80,6 +80,18 @@ export interface Cluster { trend: PostureTrend[]; } +export interface BenchmarkData { + meta: { + benchmarkId: CspFinding['rule']['benchmark']['id']; + benchmarkVersion: CspFinding['rule']['benchmark']['version']; + benchmarkName: CspFinding['rule']['benchmark']['name']; + assetCount: number; + }; + stats: Stats; + groupedFindingsEvaluation: GroupedFindingsEvaluation[]; + trend: PostureTrend[]; +} + export interface ComplianceDashboardData { stats: Stats; groupedFindingsEvaluation: GroupedFindingsEvaluation[]; @@ -87,6 +99,13 @@ export interface ComplianceDashboardData { trend: PostureTrend[]; } +export interface ComplianceDashboardDataV2 { + stats: Stats; + groupedFindingsEvaluation: GroupedFindingsEvaluation[]; + trend: PostureTrend[]; + benchmarks: BenchmarkData[]; +} + export type CspStatusCode = | 'indexed' // latest findings index exists and has results | 'indexing' // index timeout was not surpassed since installation, assumes data is being indexed diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts index 68ab9dfc698f7..834a75581519f 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts @@ -7,7 +7,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; -import { ComplianceDashboardData, PosturePolicyTemplate } from '../../../common/types'; +import { ComplianceDashboardDataV2, PosturePolicyTemplate } from '../../../common/types'; import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE, @@ -23,23 +23,25 @@ export const getStatsRoute = (policyTemplate: PosturePolicyTemplate) => { }; export const useCspmStatsApi = ( - options: UseQueryOptions + options: UseQueryOptions ) => { const { http } = useKibana().services; return useQuery( getCspmStatsKey, - () => http.get(getStatsRoute(CSPM_POLICY_TEMPLATE), { version: '1' }), + () => + http.get(getStatsRoute(CSPM_POLICY_TEMPLATE), { version: '2' }), options ); }; export const useKspmStatsApi = ( - options: UseQueryOptions + options: UseQueryOptions ) => { const { http } = useKibana().services; return useQuery( getKspmStatsKey, - () => http.get(getStatsRoute(KSPM_POLICY_TEMPLATE), { version: '1' }), + () => + http.get(getStatsRoute(KSPM_POLICY_TEMPLATE), { version: '2' }), options ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 7641745b897f4..5ea356e4a3836 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -45,6 +45,8 @@ export const LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY = 'cloudPosture:benchmark:pag export const LOCAL_STORAGE_PAGE_SIZE_RULES_KEY = 'cloudPosture:rules:pageSize'; export const LOCAL_STORAGE_DASHBOARD_CLUSTER_SORT_KEY = 'cloudPosture:complianceDashboard:clusterSort'; +export const LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY = + 'cloudPosture:complianceDashboard:benchmarkSort'; export const LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY = 'cloudPosture:findings:lastSelectedTab'; export type CloudPostureIntegrations = Record< diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 418b1c37a1bdd..4feee3a5e2287 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/constants'; -import { Cluster } from '../../common/types'; import { CISBenchmarkIcon } from './cis_benchmark_icon'; import { CompactFormattedNumber } from './compact_formatted_number'; import { useNavigateFindings } from '../common/hooks/use_navigate_findings'; +import { BenchmarkData } from '../../common/types'; // order in array will determine order of appearance in the dashboard const benchmarks = [ @@ -43,17 +43,17 @@ const benchmarks = [ ]; export const AccountsEvaluatedWidget = ({ - clusters, + benchmarkAssets, benchmarkAbbreviateAbove = 999, }: { - clusters: Cluster[]; + 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; }) => { const { euiTheme } = useEuiTheme(); - const filterClustersById = (benchmarkId: string) => { - return clusters?.filter((obj) => obj?.meta.benchmark.id === benchmarkId) || []; + const filterBenchmarksById = (benchmarkId: string) => { + return benchmarkAssets?.filter((obj) => obj?.meta.benchmarkId === benchmarkId) || []; }; const navToFindings = useNavigateFindings(); @@ -67,10 +67,10 @@ export const AccountsEvaluatedWidget = ({ }; const benchmarkElements = benchmarks.map((benchmark) => { - const clusterAmount = filterClustersById(benchmark.type).length; + const cloudAssetAmount = filterBenchmarksById(benchmark.type).length; return ( - clusterAmount > 0 && ( + cloudAssetAmount > 0 && ( { @@ -98,7 +98,7 @@ export const AccountsEvaluatedWidget = ({ diff --git a/x-pack/plugins/cloud_security_posture/public/components/chart_panel.tsx b/x-pack/plugins/cloud_security_posture/public/components/chart_panel.tsx index b6ffc9f0157b8..6c813c480ed8c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/chart_panel.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/chart_panel.tsx @@ -23,6 +23,7 @@ interface ChartPanelProps { isLoading?: boolean; isError?: boolean; rightSideItems?: ReactNode[]; + styles?: React.CSSProperties; } const Loading = () => ( @@ -54,6 +55,7 @@ export const ChartPanel: React.FC = ({ isError, children, rightSideItems, + styles, }) => { const { euiTheme } = useEuiTheme(); const renderChart = () => { @@ -63,7 +65,7 @@ export const ChartPanel: React.FC = ({ }; return ( - + diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx index 956f63ed2d3bd..2067a9c98fd23 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx @@ -30,6 +30,7 @@ import { import { FormattedDate, FormattedTime } from '@kbn/i18n-react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; import { DASHBOARD_COMPLIANCE_SCORE_CHART } from '../test_subjects'; import { statusColors } from '../../../common/constants'; import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants'; @@ -45,6 +46,123 @@ interface ComplianceScoreChartProps { onEvalCounterClick: (evaluation: Evaluation) => void; } +const CounterButtonLink = ({ + text, + count, + color, + onClick, +}: { + count: number; + text: string; + color: EuiTextProps['color']; + onClick: EuiLinkButtonProps['onClick']; +}) => { + const { euiTheme } = useEuiTheme(); + + return ( + <> + + {text} + + + + + +   + + + + ); +}; + +const CompactPercentageLabels = ({ + onEvalCounterClick, + stats, +}: { + onEvalCounterClick: (evaluation: Evaluation) => void; + stats: { totalPassed: number; totalFailed: number }; +}) => ( + <> + onEvalCounterClick(RULE_PASSED)} + tooltipContent={i18n.translate( + 'xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip', + { defaultMessage: 'Passed findings' } + )} + /> +  -  + onEvalCounterClick(RULE_FAILED)} + tooltipContent={i18n.translate( + 'xpack.csp.complianceScoreChart.counterButtonLink.failedFindingsTooltip', + { defaultMessage: 'Failed findings' } + )} + /> + +); + +const NonCompactPercentageLabels = ({ + onEvalCounterClick, + stats, +}: { + onEvalCounterClick: (evaluation: Evaluation) => void; + stats: { totalPassed: number; totalFailed: number }; +}) => { + const { euiTheme } = useEuiTheme(); + const borderLeftStyles = { borderLeft: euiTheme.border.thin, paddingLeft: euiTheme.size.m }; + return ( + + + onEvalCounterClick(RULE_PASSED)} + /> + + + onEvalCounterClick(RULE_FAILED)} + /> + + + ); +}; + const getPostureScorePercentage = (postureScore: number): string => `${Math.round(postureScore)}%`; const PercentageInfo = ({ @@ -177,27 +295,17 @@ export const ComplianceScoreChart = ({ alignItems="flexStart" style={{ paddingRight: euiTheme.size.xl }} > - onEvalCounterClick(RULE_PASSED)} - tooltipContent={i18n.translate( - 'xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip', - { defaultMessage: 'Passed findings' } - )} - /> -  -  - onEvalCounterClick(RULE_FAILED)} - tooltipContent={i18n.translate( - 'xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip', - { defaultMessage: 'Failed findings' } - )} - /> + {compact ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index 1c8da9db27871..18e5118f772e5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -32,7 +32,11 @@ import { KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, PACKAGE_NOT_INSTALLED_TEST_SUBJECT, } from '../../components/cloud_posture_page'; -import { BaseCspSetupStatus, ComplianceDashboardData, CspStatusCode } from '../../../common/types'; +import { + BaseCspSetupStatus, + ComplianceDashboardDataV2, + CspStatusCode, +} from '../../../common/types'; jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/api/use_stats_api'); @@ -779,31 +783,31 @@ describe('getDefaultTab', () => { it('returns CSPM tab if only CSPM has findings', () => { const pluginStatus = getPluginStatusMock('indexed', 'indexed') as BaseCspSetupStatus; - const cspmStats = getStatsMock(1) as ComplianceDashboardData; - const kspmStats = getStatsMock(0) as ComplianceDashboardData; + const cspmStats = getStatsMock(1) as ComplianceDashboardDataV2; + const kspmStats = getStatsMock(0) as ComplianceDashboardDataV2; expect(getDefaultTab(pluginStatus, cspmStats, kspmStats)).toEqual('cspm'); }); it('returns CSPM tab if both CSPM and KSPM has findings', () => { const pluginStatus = getPluginStatusMock('indexed', 'indexed') as BaseCspSetupStatus; - const cspmStats = getStatsMock(1) as ComplianceDashboardData; - const kspmStats = getStatsMock(1) as ComplianceDashboardData; + const cspmStats = getStatsMock(1) as ComplianceDashboardDataV2; + const kspmStats = getStatsMock(1) as ComplianceDashboardDataV2; expect(getDefaultTab(pluginStatus, cspmStats, kspmStats)).toEqual('cspm'); }); it('returns KSPM tab if only KSPM has findings', () => { const pluginStatus = getPluginStatusMock('indexed', 'indexed') as BaseCspSetupStatus; - const cspmStats = getStatsMock(0) as ComplianceDashboardData; - const kspmStats = getStatsMock(1) as ComplianceDashboardData; + const cspmStats = getStatsMock(0) as ComplianceDashboardDataV2; + const kspmStats = getStatsMock(1) as ComplianceDashboardDataV2; expect(getDefaultTab(pluginStatus, cspmStats, kspmStats)).toEqual('kspm'); }); it('when no findings preffers CSPM tab unless not-installed or unprivileged', () => { - const cspmStats = getStatsMock(0) as ComplianceDashboardData; - const kspmStats = getStatsMock(0) as ComplianceDashboardData; + const cspmStats = getStatsMock(0) as ComplianceDashboardDataV2; + const kspmStats = getStatsMock(0) as ComplianceDashboardDataV2; const CspStatusCodeArray: CspStatusCode[] = [ 'indexed', 'indexing', @@ -833,13 +837,13 @@ describe('getDefaultTab', () => { }); it('returns CSPM tab is plugin status and kspm status is not provided', () => { - const cspmStats = getStatsMock(1) as ComplianceDashboardData; + const cspmStats = getStatsMock(1) as ComplianceDashboardDataV2; expect(getDefaultTab(undefined, cspmStats, undefined)).toEqual('cspm'); }); it('returns KSPM tab is plugin status and csp status is not provided', () => { - const kspmStats = getStatsMock(1) as ComplianceDashboardData; + const kspmStats = getStatsMock(1) as ComplianceDashboardDataV2; expect(getDefaultTab(undefined, undefined, kspmStats)).toEqual('kspm'); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 8b7b0d5c841af..9e3b8df0c1d3e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -15,7 +15,7 @@ import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; import type { PosturePolicyTemplate, - ComplianceDashboardData, + ComplianceDashboardDataV2, BaseCspSetupStatus, } from '../../../common/types'; import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title'; @@ -127,7 +127,7 @@ const IntegrationPostureDashboard = ({ isIntegrationInstalled, dashboardType, }: { - complianceData: ComplianceDashboardData | undefined; + complianceData: ComplianceDashboardDataV2 | undefined; notInstalledConfig: CspNoDataPageProps; isIntegrationInstalled?: boolean; dashboardType: PosturePolicyTemplate; @@ -188,8 +188,8 @@ const IntegrationPostureDashboard = ({ export const getDefaultTab = ( pluginStatus?: BaseCspSetupStatus, - cspmStats?: ComplianceDashboardData, - kspmStats?: ComplianceDashboardData + cspmStats?: ComplianceDashboardDataV2, + kspmStats?: ComplianceDashboardDataV2 ) => { const cspmTotalFindings = cspmStats?.stats.totalFindings; const kspmTotalFindings = kspmStats?.stats.totalFindings; @@ -223,7 +223,7 @@ export const getDefaultTab = ( return preferredDashboard; }; -const determineDashboardDataRefetchInterval = (data: ComplianceDashboardData | undefined) => { +const determineDashboardDataRefetchInterval = (data: ComplianceDashboardDataV2 | undefined) => { if (data?.stats.totalFindings === 0) { return NO_FINDINGS_STATUS_REFRESH_INTERVAL_MS; } @@ -258,7 +258,7 @@ const TabContent = ({ posturetype }: { posturetype: PosturePolicyTemplate }) => let integrationLink; let dataTestSubj; let policyTemplate: PosturePolicyTemplate; - let getDashboardData: UseQueryResult; + let getDashboardData: UseQueryResult; switch (posturetype) { case POSTURE_TYPE_CSPM: diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx new file mode 100644 index 0000000000000..de982b50f48f6 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx @@ -0,0 +1,176 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { getBenchmarkIdQuery } from './benchmarks_section'; +import { BenchmarkData } from '../../../../common/types'; +import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; +import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; +import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; +export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) => { + const navToFindings = useNavigateFindings(); + + const handleBenchmarkClick = () => { + return navToFindings(getBenchmarkIdQuery(benchmark)); + }; + + const getBenchmarkInfo = ( + benchmarkId: string, + cloudAssetCount: number + ): { name: string; assetType: string } => { + const benchmarks: Record = { + cis_gcp: { + name: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisGcpBenchmarkName', + { + defaultMessage: 'CIS GCP', + } + ), + assetType: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisGcpBenchmarkAssetType', + { + defaultMessage: '{count, plural, one {# Project} other {# Projects}}', + values: { count: cloudAssetCount }, + } + ), + }, + cis_aws: { + name: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisAwsBenchmarkName', + { + defaultMessage: 'CIS AWS', + } + ), + assetType: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisAwsBenchmarkAssetType', + { + defaultMessage: '{count, plural, one {# Account} other {# Accounts}}', + values: { count: cloudAssetCount }, + } + ), + }, + cis_azure: { + name: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisAzureBenchmarkName', + { + defaultMessage: 'CIS Azure', + } + ), + assetType: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisAzureBenchmarkAssetType', + { + defaultMessage: '{count, plural, one {# Subscription} other {# Subscriptions}}', + values: { count: cloudAssetCount }, + } + ), + }, + cis_k8s: { + name: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisK8sBenchmarkName', + { + defaultMessage: 'CIS Kubernetes', + } + ), + assetType: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisK8sBenchmarkAssetType', + { + defaultMessage: '{count, plural, one {# Cluster} other {# Clusters}}', + values: { count: cloudAssetCount }, + } + ), + }, + cis_eks: { + name: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisEksBenchmarkName', + { + defaultMessage: 'CIS EKS', + } + ), + assetType: i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisEksBenchmarkAssetType', + { + defaultMessage: '{count, plural, one {# Cluster} other {# Clusters}}', + values: { count: cloudAssetCount }, + } + ), + }, + }; + return benchmarks[benchmarkId]; + }; + + const cisTooltip = i18n.translate( + 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisBenchmarkTooltip', + { + defaultMessage: 'Center of Internet Security', + } + ); + + const benchmarkInfo = getBenchmarkInfo(benchmark.meta.benchmarkId, benchmark.meta.assetCount); + + const benchmarkId = benchmark.meta.benchmarkId; + const benchmarkVersion = benchmark.meta.benchmarkVersion; + const benchmarkName = benchmark.meta.benchmarkName; + + return ( + + + + + + + + + } + > + + +
{benchmarkInfo.name}
+
+
+
+ + + {benchmarkInfo.assetType} + +
+ + + + + + + + {benchmarkVersion} + + + +
+ ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx index 5293eec114fc6..79b644af37795 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BenchmarksSection } from './benchmarks_section'; -import { getMockDashboardData, getClusterMockData } from '../mock'; +import { getMockDashboardData, getBenchmarkMockData } from '../mock'; import { TestProvider } from '../../../test/test_provider'; import { KSPM_POLICY_TEMPLATE } from '../../../../common/constants'; import { @@ -30,22 +30,22 @@ describe('', () => { describe('Sorting', () => { const mockDashboardDataCopy = getMockDashboardData(); - const clusterMockDataCopy = getClusterMockData(); - clusterMockDataCopy.stats.postureScore = 50; - clusterMockDataCopy.meta.assetIdentifierId = '1'; + const benchmarkMockDataCopy = getBenchmarkMockData(); + benchmarkMockDataCopy.stats.postureScore = 50; + benchmarkMockDataCopy.meta.benchmarkId = 'cis_aws'; - const clusterMockDataCopy1 = getClusterMockData(); - clusterMockDataCopy1.stats.postureScore = 95; - clusterMockDataCopy1.meta.assetIdentifierId = '2'; + const benchmarkMockDataCopy1 = getBenchmarkMockData(); + benchmarkMockDataCopy1.stats.postureScore = 95; + benchmarkMockDataCopy1.meta.benchmarkId = 'cis_azure'; - const clusterMockDataCopy2 = getClusterMockData(); - clusterMockDataCopy2.stats.postureScore = 45; - clusterMockDataCopy2.meta.assetIdentifierId = '3'; + const benchmarkMockDataCopy2 = getBenchmarkMockData(); + benchmarkMockDataCopy2.stats.postureScore = 45; + benchmarkMockDataCopy2.meta.benchmarkId = 'cis_gcp'; - mockDashboardDataCopy.clusters = [ - clusterMockDataCopy, - clusterMockDataCopy1, - clusterMockDataCopy2, + mockDashboardDataCopy.benchmarks = [ + benchmarkMockDataCopy, + benchmarkMockDataCopy1, + benchmarkMockDataCopy2, ]; it('sorts by ascending order of compliance scores', () => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx index 96da28e0c8012..2ac91288475de 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx @@ -7,102 +7,102 @@ import React, { useMemo } from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import type { EuiIconProps } from '@elastic/eui'; +import { EuiIconProps, EuiPanel } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import type { - Cluster, - ComplianceDashboardData, + BenchmarkData, + ComplianceDashboardDataV2, Evaluation, PosturePolicyTemplate, } from '../../../../common/types'; -import { LOCAL_STORAGE_DASHBOARD_CLUSTER_SORT_KEY } from '../../../common/constants'; import { RisksTable } from '../compliance_charts/risks_table'; -import { - CSPM_POLICY_TEMPLATE, - KSPM_POLICY_TEMPLATE, - RULE_FAILED, -} from '../../../../common/constants'; +import { RULE_FAILED } from '../../../../common/constants'; +import { LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY } from '../../../common/constants'; import { NavFilter, useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; -import { ClusterDetailsBox } from './cluster_details_box'; import { dashboardColumnsGrow, getPolicyTemplateQuery } from './summary_section'; import { DASHBOARD_TABLE_COLUMN_SCORE_TEST_ID, DASHBOARD_TABLE_HEADER_SCORE_TEST_ID, } from '../test_subjects'; import { ComplianceScoreChart } from '../compliance_charts/compliance_score_chart'; +import { BenchmarkDetailsBox } from './benchmark_details_box'; +const BENCHMARK_DEFAULT_SORT_ORDER = 'asc'; -const CLUSTER_DEFAULT_SORT_ORDER = 'asc'; - -export const getClusterIdQuery = (cluster: Cluster): NavFilter => { - if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) { - // TODO: remove assertion after typing CspFinding as discriminating union - return { 'cloud.account.id': cluster.meta.cloud!.account.id }; - } - return { cluster_id: cluster.meta.assetIdentifierId }; +export const getBenchmarkIdQuery = (benchmark: BenchmarkData): NavFilter => { + return { + 'rule.benchmark.id': benchmark.meta.benchmarkId, + 'rule.benchmark.version': benchmark.meta.benchmarkVersion, + }; }; export const BenchmarksSection = ({ complianceData, dashboardType, }: { - complianceData: ComplianceDashboardData; + complianceData: ComplianceDashboardDataV2; dashboardType: PosturePolicyTemplate; }) => { const { euiTheme } = useEuiTheme(); const navToFindings = useNavigateFindings(); - const [clusterSorting, setClusterSorting] = useLocalStorage<'asc' | 'desc'>( - LOCAL_STORAGE_DASHBOARD_CLUSTER_SORT_KEY, - CLUSTER_DEFAULT_SORT_ORDER + const [benchmarkSorting, setBenchmarkSorting] = useLocalStorage<'asc' | 'desc'>( + LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY, + BENCHMARK_DEFAULT_SORT_ORDER ); - const isClusterSortingAsc = clusterSorting === 'asc'; + const isBenchmarkSortingAsc = benchmarkSorting === 'asc'; - const clusterSortingIcon: EuiIconProps['type'] = isClusterSortingAsc ? 'sortUp' : 'sortDown'; + const benchmarkSortingIcon: EuiIconProps['type'] = isBenchmarkSortingAsc ? 'sortUp' : 'sortDown'; - const navToFindingsByClusterAndEvaluation = (cluster: Cluster, evaluation: Evaluation) => { + const navToFindingsByBenchmarkAndEvaluation = ( + benchmark: BenchmarkData, + evaluation: Evaluation + ) => { navToFindings({ ...getPolicyTemplateQuery(dashboardType), - ...getClusterIdQuery(cluster), + ...getBenchmarkIdQuery(benchmark), 'result.evaluation': evaluation, }); }; - const navToFailedFindingsByClusterAndSection = (cluster: Cluster, ruleSection: string) => { + const navToFailedFindingsByBenchmarkAndSection = ( + benchmark: BenchmarkData, + ruleSection: string + ) => { navToFindings({ ...getPolicyTemplateQuery(dashboardType), - ...getClusterIdQuery(cluster), + ...getBenchmarkIdQuery(benchmark), 'rule.section': ruleSection, 'result.evaluation': RULE_FAILED, }); }; - const navToFailedFindingsByCluster = (cluster: Cluster) => { - navToFindingsByClusterAndEvaluation(cluster, RULE_FAILED); + const navToFailedFindingsByBenchmark = (benchmark: BenchmarkData) => { + navToFindingsByBenchmarkAndEvaluation(benchmark, RULE_FAILED); }; - const toggleClustersSortingDirection = () => { - setClusterSorting(isClusterSortingAsc ? 'desc' : 'asc'); + const toggleBenchmarkSortingDirection = () => { + setBenchmarkSorting(isBenchmarkSortingAsc ? 'desc' : 'asc'); }; - const clusters = useMemo(() => { - return [...complianceData.clusters].sort((clusterA, clusterB) => - isClusterSortingAsc - ? clusterA.stats.postureScore - clusterB.stats.postureScore - : clusterB.stats.postureScore - clusterA.stats.postureScore + const benchmarks = useMemo(() => { + return [...complianceData.benchmarks].sort((benchmarkA, benchmarkB) => + isBenchmarkSortingAsc + ? benchmarkA.stats.postureScore - benchmarkB.stats.postureScore + : benchmarkB.stats.postureScore - benchmarkA.stats.postureScore ); - }, [complianceData.clusters, isClusterSortingAsc]); + }, [complianceData.benchmarks, isBenchmarkSortingAsc]); return ( - <> +
- {dashboardType === KSPM_POLICY_TEMPLATE ? ( - - ) : ( - - )} +
@@ -155,18 +148,20 @@ export const BenchmarksSection = ({
- {clusters.map((cluster) => ( + {benchmarks.map((benchmark) => ( - + - navToFindingsByClusterAndEvaluation(cluster, evaluation) + navToFindingsByBenchmarkAndEvaluation(benchmark, evaluation) } /> @@ -193,27 +188,23 @@ export const BenchmarksSection = ({ > - navToFailedFindingsByClusterAndSection(cluster, resourceTypeName) + navToFailedFindingsByBenchmarkAndSection(benchmark, resourceTypeName) } viewAllButtonTitle={i18n.translate( - 'xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle', + 'xpack.csp.dashboard.risksTable.benchmarkCardViewAllButtonTitle', { - defaultMessage: 'View all failed findings for this {postureAsset}', - values: { - postureAsset: - dashboardType === CSPM_POLICY_TEMPLATE ? 'cloud account' : 'cluster', - }, + defaultMessage: 'View all failed findings for this benchmark', } )} - onViewAllClick={() => navToFailedFindingsByCluster(cluster)} + onViewAllClick={() => navToFailedFindingsByBenchmark(benchmark)} /> ))} - +
); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx deleted file mode 100644 index 7b42445d26b99..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, - EuiTitle, - EuiToolTip, - useEuiTheme, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import moment from 'moment'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { getClusterIdQuery } from './benchmarks_section'; -import { CSPM_POLICY_TEMPLATE, INTERNAL_FEATURE_FLAGS } from '../../../../common/constants'; -import { Cluster } from '../../../../common/types'; -import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; -import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; - -const defaultClusterTitle = i18n.translate( - 'xpack.csp.dashboard.benchmarkSection.defaultClusterTitle', - { defaultMessage: 'ID' } -); - -const getClusterTitle = (cluster: Cluster) => { - if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) { - return cluster.meta.cloud?.account.name; - } - - return cluster.meta.cluster?.name; -}; - -const getClusterId = (cluster: Cluster) => { - const assetIdentifierId = cluster.meta.assetIdentifierId; - if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) return assetIdentifierId; - return assetIdentifierId.slice(0, 6); -}; - -export const ClusterDetailsBox = ({ cluster }: { cluster: Cluster }) => { - const { euiTheme } = useEuiTheme(); - const navToFindings = useNavigateFindings(); - - const assetId = getClusterId(cluster); - const title = getClusterTitle(cluster) || defaultClusterTitle; - - const handleClusterTitleClick = () => { - return navToFindings(getClusterIdQuery(cluster)); - }; - - return ( - - - - - - - - - } - > - - -
- -
-
-
-
- - - -
- - - - {INTERNAL_FEATURE_FLAGS.showManageRulesMock && ( - - - - - - )} -
- ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index a0170705b1ec5..ca4a55c45ebdd 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -6,7 +6,13 @@ */ import React, { useMemo } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFlexItemProps } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlexItemProps, + useEuiTheme, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { useCspIntegrationLink } from '../../../common/navigation/use_csp_integration_link'; @@ -16,7 +22,7 @@ import { CompactFormattedNumber } from '../../../components/compact_formatted_nu import { ChartPanel } from '../../../components/chart_panel'; import { ComplianceScoreChart } from '../compliance_charts/compliance_score_chart'; import type { - ComplianceDashboardData, + ComplianceDashboardDataV2, Evaluation, PosturePolicyTemplate, } from '../../../../common/types'; @@ -48,12 +54,14 @@ export const SummarySection = ({ complianceData, }: { dashboardType: PosturePolicyTemplate; - complianceData: ComplianceDashboardData; + complianceData: ComplianceDashboardDataV2; }) => { const navToFindings = useNavigateFindings(); const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); + const { euiTheme } = useEuiTheme(); + const handleEvalCounterClick = (evaluation: Evaluation) => { navToFindings({ 'result.evaluation': evaluation, ...getPolicyTemplateQuery(dashboardType) }); }; @@ -84,7 +92,7 @@ export const SummarySection = ({ 'xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription', { defaultMessage: 'Accounts Evaluated' } ), - title: , + title: , button: ( ({ +export const getMockDashboardData = () => ({ + ...mockDashboardData, +}); + +export const getBenchmarkMockData = (): BenchmarkData => ({ meta: { - assetIdentifierId: '8f9c5b98-cc02-4827-8c82-316e2cc25870', - lastUpdate: '2022-11-07T13:14:34.990Z', - cloud: { - provider: 'aws', - account: { - name: 'build-security-dev', - id: '704479110758', - }, - }, - benchmark: { - name: 'CIS Amazon Web Services Foundations', - rule_number: '1.4', - id: 'cis_aws', - posture_type: 'cspm', - version: 'v1.5.0', - }, - cluster: { - name: '8f9c5b98-cc02-4827-8c82-316e2cc25870', - }, + benchmarkId: 'cis_aws', + benchmarkVersion: '1.2.3', + benchmarkName: 'CIS AWS Foundations Benchmark', + assetCount: 153, }, stats: { totalFailed: 17, @@ -104,11 +93,7 @@ export const getClusterMockData = (): Cluster => ({ ], }); -export const getMockDashboardData = () => ({ - ...mockDashboardData, -}); - -export const mockDashboardData: ComplianceDashboardData = { +export const mockDashboardData: ComplianceDashboardDataV2 = { stats: { totalFailed: 17, totalPassed: 155, @@ -167,7 +152,7 @@ export const mockDashboardData: ComplianceDashboardData = { postureScore: 50.0, }, ], - clusters: [getClusterMockData()], + benchmarks: [getBenchmarkMockData()], trend: [ { timestamp: '2022-05-22T11:03:00.000Z', diff --git a/x-pack/plugins/cloud_security_posture/server/lib/mapping_field_util.test.ts b/x-pack/plugins/cloud_security_posture/server/lib/mapping_field_util.test.ts new file mode 100644 index 0000000000000..848a7ac0aa399 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/lib/mapping_field_util.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { + toBenchmarkDocFieldKey, + toBenchmarkMappingFieldKey, + MAPPING_VERSION_DELIMITER, +} from './mapping_field_util'; // replace 'yourFile' with the actual file name + +describe('Benchmark Field Key Functions', () => { + const sampleBenchmarkId = 'cis_aws'; + const sampleBenchmarkVersion = '1.0.0'; + + it('toBenchmarkDocFieldKey should keep the same benchmark id and version key for benchmark document', () => { + const result = toBenchmarkDocFieldKey(sampleBenchmarkId, sampleBenchmarkVersion); + const expected = `${sampleBenchmarkId};${sampleBenchmarkVersion}`; + expect(result).toEqual(expected); + }); + + it('toBenchmarkDocFieldKey should convert benchmark version with . delimiter correctly', () => { + const benchmarkVersionWithDelimiter = '1_0_0'; + const result = toBenchmarkDocFieldKey(sampleBenchmarkId, benchmarkVersionWithDelimiter); + const expected = `${sampleBenchmarkId};1.0.0`; + expect(result).toEqual(expected); + }); + + it('toBenchmarkMappingFieldKey should convert benchmark version with _ delimiter correctly', () => { + const result = toBenchmarkMappingFieldKey(sampleBenchmarkVersion); + const expected = '1_0_0'; + expect(result).toEqual(expected); + }); + + it('toBenchmarkMappingFieldKey should handle benchmark version with dots correctly', () => { + const benchmarkVersionWithDots = '1.0.0'; + const result = toBenchmarkMappingFieldKey(benchmarkVersionWithDots); + const expected = '1_0_0'; + expect(result).toEqual(expected); + }); + + it('MAPPING_VERSION_DELIMITER should be an underscore', () => { + expect(MAPPING_VERSION_DELIMITER).toBe('_'); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/server/lib/mapping_field_util.ts b/x-pack/plugins/cloud_security_posture/server/lib/mapping_field_util.ts new file mode 100644 index 0000000000000..7cc392d3da748 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/lib/mapping_field_util.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export const MAPPING_VERSION_DELIMITER = '_'; + +/* + * The latest finding index store benchmark version field value `v1.2.0` + * when we store the benchmark id and version field name in the benchmark scores index, + * we need benchmark version with _ delimiter to avoid JSON mapping for each dot notation + * to be read as key. e.g. `v1.2.0` will be `v1_2_0` + */ + +export const toBenchmarkDocFieldKey = (benchmarkId: string, benchmarkVersion: string) => + benchmarkVersion.includes(MAPPING_VERSION_DELIMITER) + ? `${benchmarkId};${benchmarkVersion.replaceAll('_', '.')}` + : `${benchmarkId};${benchmarkVersion}`; + +export const toBenchmarkMappingFieldKey = (benchmarkVersion: string) => + `${benchmarkVersion.replaceAll('.', '_')}`; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts index b827f5a31b4ee..88c7afd5aca11 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts @@ -14,6 +14,7 @@ import type { PosturePolicyTemplate, ComplianceDashboardData, GetComplianceDashboardRequest, + ComplianceDashboardDataV2, } from '../../../common/types'; import { LATEST_FINDINGS_INDEX_DEFAULT_NS, STATS_ROUTE_PATH } from '../../../common/constants'; import { getGroupedFindingsEvaluation } from './get_grouped_findings_evaluation'; @@ -21,6 +22,8 @@ import { ClusterWithoutTrend, getClusters } from './get_clusters'; import { getStats } from './get_stats'; import { CspRouter } from '../../types'; import { getTrends, Trends } from './get_trends'; +import { BenchmarkWithoutTrend, getBenchmarks } from './get_benchmarks'; +import { toBenchmarkDocFieldKey } from '../../lib/mapping_field_util'; export interface KeyDocCount { key: TKey; @@ -36,6 +39,23 @@ const getClustersTrends = (clustersWithoutTrends: ClusterWithoutTrend[], trends: })), })); +const getBenchmarksTrends = (benchmarksWithoutTrends: BenchmarkWithoutTrend[], trends: Trends) => { + return benchmarksWithoutTrends.map((benchmark) => ({ + ...benchmark, + trend: trends.map(({ timestamp, benchmarks: benchmarksTrendData }) => { + const benchmarkIdVersion = toBenchmarkDocFieldKey( + benchmark.meta.benchmarkId, + benchmark.meta.benchmarkVersion + ); + + return { + timestamp, + ...benchmarksTrendData[benchmarkIdVersion], + }; + }), + })); +}; + const getSummaryTrend = (trends: Trends) => trends.map(({ timestamp, summary }) => ({ timestamp, ...summary })); @@ -56,6 +76,7 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter) => }, async (context, request, response) => { const cspContext = await context.csp; + const logger = cspContext.logger; try { const esClient = cspContext.esClient.asCurrentUser; @@ -79,16 +100,16 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter) => const [stats, groupedFindingsEvaluation, clustersWithoutTrends, trends] = await Promise.all([ - getStats(esClient, query, pitId, runtimeMappings), - getGroupedFindingsEvaluation(esClient, query, pitId, runtimeMappings), - getClusters(esClient, query, pitId, runtimeMappings), - getTrends(esClient, policyTemplate), + getStats(esClient, query, pitId, runtimeMappings, logger), + getGroupedFindingsEvaluation(esClient, query, pitId, runtimeMappings, logger), + getClusters(esClient, query, pitId, runtimeMappings, logger), + getTrends(esClient, policyTemplate, logger), ]); // Try closing the PIT, if it fails we can safely ignore the error since it closes itself after the keep alive // ends. Not waiting on the promise returned from the `closePointInTime` call to avoid delaying the request esClient.closePointInTime({ id: pitId }).catch((err) => { - cspContext.logger.warn(`Could not close PIT for stats endpoint: ${err}`); + logger.warn(`Could not close PIT for stats endpoint: ${err}`); }); const clusters = getClustersTrends(clustersWithoutTrends, trends); @@ -106,7 +127,80 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter) => }); } catch (err) { const error = transformError(err); - cspContext.logger.error(`Error while fetching CSP stats: ${err}`); + logger.error(`Error while fetching CSP stats: ${err}`); + logger.error(err.stack); + + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, + }); + } + } + ) + .addVersion( + { + version: '2', + validate: { + request: { + params: getComplianceDashboardSchema, + }, + }, + }, + async (context, request, response) => { + const cspContext = await context.csp; + const logger = cspContext.logger; + + try { + const esClient = cspContext.esClient.asCurrentUser; + + const { id: pitId } = await esClient.openPointInTime({ + index: LATEST_FINDINGS_INDEX_DEFAULT_NS, + keep_alive: '30s', + }); + + const params: GetComplianceDashboardRequest = request.params; + const policyTemplate = params.policy_template as PosturePolicyTemplate; + + // runtime mappings create the `safe_posture_type` field, which equals to `kspm` or `cspm` based on the value and existence of the `posture_type` field which was introduced at 8.7 + // the `query` is then being passed to our getter functions to filter per posture type even for older findings before 8.7 + const runtimeMappings: MappingRuntimeFields = getSafePostureTypeRuntimeMapping(); + const query: QueryDslQueryContainer = { + bool: { + filter: [{ term: { safe_posture_type: policyTemplate } }], + }, + }; + + const [stats, groupedFindingsEvaluation, benchmarksWithoutTrends, trends] = + await Promise.all([ + getStats(esClient, query, pitId, runtimeMappings, logger), + getGroupedFindingsEvaluation(esClient, query, pitId, runtimeMappings, logger), + getBenchmarks(esClient, query, pitId, runtimeMappings, logger), + getTrends(esClient, policyTemplate, logger), + ]); + + // Try closing the PIT, if it fails we can safely ignore the error since it closes itself after the keep alive + // ends. Not waiting on the promise returned from the `closePointInTime` call to avoid delaying the request + esClient.closePointInTime({ id: pitId }).catch((err) => { + logger.warn(`Could not close PIT for stats endpoint: ${err}`); + }); + + const benchmarks = getBenchmarksTrends(benchmarksWithoutTrends, trends); + const trend = getSummaryTrend(trends); + + const body: ComplianceDashboardDataV2 = { + stats, + groupedFindingsEvaluation, + benchmarks, + trend, + }; + + return response.ok({ + body, + }); + } catch (err) { + const error = transformError(err); + logger.error(`Error while fetching v2 CSP stats: ${err}`); + logger.error(err.stack); return response.customError({ body: { message: error.message }, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_benchmarks.test.ts new file mode 100644 index 0000000000000..cf4d1632a6b50 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_benchmarks.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { BenchmarkBucket, getBenchmarksFromAggs } from './get_benchmarks'; + +const mockBenchmarkBuckets: BenchmarkBucket[] = [ + { + key: 'cis_aws', + doc_count: 12, + aggs_by_benchmark_version: { + buckets: [ + { + key: 'v1.5.0', + doc_count: 12, + asset_count: { + value: 1, + }, + aggs_by_resource_type: { + buckets: [ + { + key: 'foo_type', + doc_count: 6, + passed_findings: { + doc_count: 3, + }, + failed_findings: { + doc_count: 3, + }, + score: { + value: 0.5, + }, + }, + { + key: 'boo_type', + doc_count: 6, + passed_findings: { + doc_count: 3, + }, + failed_findings: { + doc_count: 3, + }, + score: { + value: 0.5, + }, + }, + ], + }, + aggs_by_benchmark_name: { + buckets: [ + { + key: 'CIS Amazon Web Services Foundations', + doc_count: 12, + }, + ], + }, + passed_findings: { + doc_count: 6, + }, + failed_findings: { + doc_count: 6, + }, + }, + ], + }, + }, +]; + +describe('getBenchmarksFromAggs', () => { + it('should return value matching ComplianceDashboardDataV2["benchmarks"]', async () => { + const benchmarks = getBenchmarksFromAggs(mockBenchmarkBuckets); + expect(benchmarks).toEqual([ + { + meta: { + benchmarkId: 'cis_aws', + benchmarkVersion: 'v1.5.0', + benchmarkName: 'CIS Amazon Web Services Foundations', + assetCount: 1, + }, + stats: { + totalFindings: 12, + totalFailed: 6, + totalPassed: 6, + postureScore: 50.0, + }, + groupedFindingsEvaluation: [ + { + name: 'foo_type', + totalFindings: 6, + totalFailed: 3, + totalPassed: 3, + postureScore: 50.0, + }, + { + name: 'boo_type', + totalFindings: 6, + totalFailed: 3, + totalPassed: 3, + postureScore: 50.0, + }, + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_benchmarks.ts new file mode 100644 index 0000000000000..b1f8335a61866 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_benchmarks.ts @@ -0,0 +1,151 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import type { + AggregationsMultiBucketAggregateBase as Aggregation, + QueryDslQueryContainer, + SearchRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import type { Logger } from '@kbn/core/server'; +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { BenchmarkData } from '../../../common/types'; +import { + failedFindingsAggQuery, + BenchmarkVersionQueryResult, + getPostureStatsFromAggs, +} from './get_grouped_findings_evaluation'; +import { findingsEvaluationAggsQuery, getStatsFromFindingsEvaluationsAggs } from './get_stats'; +import { KeyDocCount } from './compliance_dashboard'; +import { getIdentifierRuntimeMapping } from '../../../common/runtime_mappings/get_identifier_runtime_mapping'; + +export interface BenchmarkBucket extends KeyDocCount { + aggs_by_benchmark_version: Aggregation; +} + +interface BenchmarkQueryResult extends KeyDocCount { + aggs_by_benchmark: Aggregation; +} + +export type BenchmarkWithoutTrend = Omit; + +const MAX_BENCHMARKS = 500; + +export const getBenchmarksQuery = ( + query: QueryDslQueryContainer, + pitId: string, + runtimeMappings: MappingRuntimeFields +): SearchRequest => ({ + size: 0, + runtime_mappings: { ...runtimeMappings, ...getIdentifierRuntimeMapping() }, + query, + aggs: { + aggs_by_benchmark: { + terms: { + field: 'rule.benchmark.id', + order: { + _count: 'desc', + }, + size: MAX_BENCHMARKS, + }, + aggs: { + aggs_by_benchmark_version: { + terms: { + field: 'rule.benchmark.version', + }, + aggs: { + aggs_by_benchmark_name: { + terms: { + field: 'rule.benchmark.name', + }, + }, + asset_count: { + cardinality: { + field: 'asset_identifier', + }, + }, + // Result evalution for passed or failed findings + ...findingsEvaluationAggsQuery, + // CIS Section Compliance Score and Failed Findings + ...failedFindingsAggQuery, + }, + }, + }, + }, + }, + pit: { + id: pitId, + }, +}); + +export const getBenchmarksFromAggs = (benchmarks: BenchmarkBucket[]) => { + return benchmarks.flatMap((benchmarkAggregation: BenchmarkBucket) => { + const benchmarkId = benchmarkAggregation.key; + const versions = benchmarkAggregation.aggs_by_benchmark_version.buckets; + if (!Array.isArray(versions)) throw new Error('missing aggs by benchmark version'); + + return versions.map((version: BenchmarkVersionQueryResult) => { + const benchmarkVersion = version.key; + const assetCount = version.asset_count.value; + const resourcesTypesAggs = version.aggs_by_resource_type.buckets; + + let benchmarkName = ''; + + if (!Array.isArray(version.aggs_by_benchmark_name.buckets)) + throw new Error('missing aggs by benchmark name'); + + if (version.aggs_by_benchmark_name && version.aggs_by_benchmark_name.buckets.length > 0) { + benchmarkName = version.aggs_by_benchmark_name.buckets[0].key; + } + + if (!Array.isArray(resourcesTypesAggs)) + throw new Error('missing aggs by resource type per benchmark'); + + const { passed_findings: passedFindings, failed_findings: failedFindings } = version; + const stats = getStatsFromFindingsEvaluationsAggs({ + passed_findings: passedFindings, + failed_findings: failedFindings, + }); + + const groupedFindingsEvaluation = getPostureStatsFromAggs(resourcesTypesAggs); + + return { + meta: { + benchmarkId, + benchmarkVersion, + benchmarkName, + assetCount, + }, + stats, + groupedFindingsEvaluation, + }; + }); + }); +}; + +export const getBenchmarks = async ( + esClient: ElasticsearchClient, + query: QueryDslQueryContainer, + pitId: string, + runtimeMappings: MappingRuntimeFields, + logger: Logger +): Promise => { + try { + const queryResult = await esClient.search( + getBenchmarksQuery(query, pitId, runtimeMappings) + ); + + const benchmarks = queryResult.aggregations?.aggs_by_benchmark.buckets; + if (!Array.isArray(benchmarks)) throw new Error('missing aggs by benchmark id'); + + return getBenchmarksFromAggs(benchmarks); + } catch (err) { + logger.error(`Failed to fetch benchmark stats ${err.message}`); + logger.error(err); + throw err; + } +}; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts index ae40f05258634..51d5c71673ed1 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts @@ -13,13 +13,11 @@ import type { AggregationsTopHitsAggregate, SearchHit, } from '@elastic/elasticsearch/lib/api/types'; +import type { Logger } from '@kbn/core/server'; import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import { CspFinding } from '../../../common/schemas/csp_finding'; import type { Cluster } from '../../../common/types'; -import { - getFailedFindingsFromAggs, - failedFindingsAggQuery, -} from './get_grouped_findings_evaluation'; +import { getPostureStatsFromAggs, failedFindingsAggQuery } from './get_grouped_findings_evaluation'; import type { FailedFindingsQueryResult } from './get_grouped_findings_evaluation'; import { findingsEvaluationAggsQuery, getStatsFromFindingsEvaluationsAggs } from './get_stats'; import { KeyDocCount } from './compliance_dashboard'; @@ -99,7 +97,7 @@ export const getClustersFromAggs = (clusters: ClusterBucket[]): ClusterWithoutTr const resourcesTypesAggs = clusterBucket.aggs_by_resource_type.buckets; if (!Array.isArray(resourcesTypesAggs)) throw new Error('missing aggs by resource type per cluster'); - const groupedFindingsEvaluation = getFailedFindingsFromAggs(resourcesTypesAggs); + const groupedFindingsEvaluation = getPostureStatsFromAggs(resourcesTypesAggs); return { meta, @@ -112,14 +110,21 @@ export const getClusters = async ( esClient: ElasticsearchClient, query: QueryDslQueryContainer, pitId: string, - runtimeMappings: MappingRuntimeFields + runtimeMappings: MappingRuntimeFields, + logger: Logger ): Promise => { - const queryResult = await esClient.search( - getClustersQuery(query, pitId, runtimeMappings) - ); + try { + const queryResult = await esClient.search( + getClustersQuery(query, pitId, runtimeMappings) + ); - const clusters = queryResult.aggregations?.aggs_by_asset_identifier.buckets; - if (!Array.isArray(clusters)) throw new Error('missing aggs by cluster id'); + const clusters = queryResult.aggregations?.aggs_by_asset_identifier.buckets; + if (!Array.isArray(clusters)) throw new Error('missing aggs by cluster id'); - return getClustersFromAggs(clusters); + return getClustersFromAggs(clusters); + } catch (err) { + logger.error(`Failed to fetch cluster stats ${err.message}`); + logger.error(err); + throw err; + } }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.test.ts index 6af6d97f51e26..5ebc5231dee6a 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { getFailedFindingsFromAggs, FailedFindingsBucket } from './get_grouped_findings_evaluation'; +import { getPostureStatsFromAggs, PostureStatsBucket } from './get_grouped_findings_evaluation'; -const resourceTypeBuckets: FailedFindingsBucket[] = [ +const resourceTypeBuckets: PostureStatsBucket[] = [ { key: 'foo_type', doc_count: 41, @@ -36,9 +36,9 @@ const resourceTypeBuckets: FailedFindingsBucket[] = [ }, ]; -describe('getFailedFindingsFromAggs', () => { +describe('getPostureStatsFromAggs', () => { it('should return value matching ComplianceDashboardData["resourcesTypes"]', async () => { - const resourceTypes = getFailedFindingsFromAggs(resourceTypeBuckets); + const resourceTypes = getPostureStatsFromAggs(resourceTypeBuckets); expect(resourceTypes).toEqual([ { name: 'foo_type', diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts index 239801350c7af..74b239f14d242 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts @@ -11,16 +11,30 @@ import type { QueryDslQueryContainer, SearchRequest, } from '@elastic/elasticsearch/lib/api/types'; +import type { Logger } from '@kbn/core/server'; import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import { calculatePostureScore } from '../../../common/utils/helpers'; import type { ComplianceDashboardData } from '../../../common/types'; import { KeyDocCount } from './compliance_dashboard'; export interface FailedFindingsQueryResult { - aggs_by_resource_type: Aggregation; + aggs_by_resource_type: Aggregation; } -export interface FailedFindingsBucket extends KeyDocCount { +export interface BenchmarkVersionQueryResult extends KeyDocCount, FailedFindingsQueryResult { + failed_findings: { + doc_count: number; + }; + passed_findings: { + doc_count: number; + }; + asset_count: { + value: number; + }; + aggs_by_benchmark_name: Aggregation; +} + +export interface PostureStatsBucket extends KeyDocCount { failed_findings: { doc_count: number; }; @@ -79,8 +93,8 @@ export const getRisksEsQuery = ( }, }); -export const getFailedFindingsFromAggs = ( - queryResult: FailedFindingsBucket[] +export const getPostureStatsFromAggs = ( + queryResult: PostureStatsBucket[] ): ComplianceDashboardData['groupedFindingsEvaluation'] => queryResult.map((bucket) => { const totalPassed = bucket.passed_findings.doc_count || 0; @@ -99,16 +113,23 @@ export const getGroupedFindingsEvaluation = async ( esClient: ElasticsearchClient, query: QueryDslQueryContainer, pitId: string, - runtimeMappings: MappingRuntimeFields + runtimeMappings: MappingRuntimeFields, + logger: Logger ): Promise => { - const resourceTypesQueryResult = await esClient.search( - getRisksEsQuery(query, pitId, runtimeMappings) - ); + try { + const resourceTypesQueryResult = await esClient.search( + getRisksEsQuery(query, pitId, runtimeMappings) + ); - const ruleSections = resourceTypesQueryResult.aggregations?.aggs_by_resource_type.buckets; - if (!Array.isArray(ruleSections)) { - return []; - } + const ruleSections = resourceTypesQueryResult.aggregations?.aggs_by_resource_type.buckets; + if (!Array.isArray(ruleSections)) { + return []; + } - return getFailedFindingsFromAggs(ruleSections); + return getPostureStatsFromAggs(ruleSections); + } catch (err) { + logger.error(`Failed to fetch findings stats ${err.message}`); + logger.error(err); + throw err; + } }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts index 2f0e1c1b17102..f639f8a7e1421 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import type { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { Logger } from '@kbn/core/server'; import { calculatePostureScore } from '../../../common/utils/helpers'; import type { ComplianceDashboardData } from '../../../common/types'; @@ -81,14 +82,21 @@ export const getStats = async ( esClient: ElasticsearchClient, query: QueryDslQueryContainer, pitId: string, - runtimeMappings: MappingRuntimeFields + runtimeMappings: MappingRuntimeFields, + logger: Logger ): Promise => { - const evaluationsQueryResult = await esClient.search( - getEvaluationsQuery(query, pitId, runtimeMappings) - ); + try { + const evaluationsQueryResult = await esClient.search( + getEvaluationsQuery(query, pitId, runtimeMappings) + ); - const findingsEvaluations = evaluationsQueryResult.aggregations; - if (!findingsEvaluations) throw new Error('missing findings evaluations'); + const findingsEvaluations = evaluationsQueryResult.aggregations; + if (!findingsEvaluations) throw new Error('missing findings evaluations'); - return getStatsFromFindingsEvaluationsAggs(findingsEvaluations); + return getStatsFromFindingsEvaluationsAggs(findingsEvaluations); + } catch (err) { + logger.error(`Failed to fetch stats ${err.message}`); + logger.error(err); + throw err; + } }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.test.ts index 127cf3e1a3f80..f26760221292b 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getTrendsFromQueryResult, ScoreTrendDoc } from './get_trends'; +import { formatTrends, ScoreTrendDoc } from './get_trends'; const trendDocs: ScoreTrendDoc[] = [ { @@ -20,6 +20,15 @@ const trendDocs: ScoreTrendDoc[] = [ failed_findings: 15, }, }, + score_by_benchmark_id: { + cis_gcp: { + v2_0_0: { + total_findings: 6, + passed_findings: 3, + failed_findings: 3, + }, + }, + }, }, { '@timestamp': '2022-04-06T15:00:00Z', @@ -38,22 +47,27 @@ const trendDocs: ScoreTrendDoc[] = [ failed_findings: 5, }, }, - }, - { - '@timestamp': '2022-04-05T15:30:00Z', - total_findings: 30, - passed_findings: 25, - failed_findings: 5, - score_by_cluster_id: { - forth_cluster_id: { - total_findings: 25, - passed_findings: 25, - failed_findings: 0, + score_by_benchmark_id: { + cis_gcp: { + v2_0_0: { + total_findings: 6, + passed_findings: 3, + failed_findings: 3, + }, }, - fifth_cluster_id: { - total_findings: 5, - passed_findings: 0, - failed_findings: 5, + cis_azure: { + v2_0_0: { + total_findings: 6, + passed_findings: 3, + failed_findings: 3, + }, + }, + cis_aws: { + v1_5_0: { + total_findings: 6, + passed_findings: 3, + failed_findings: 3, + }, }, }, }, @@ -61,7 +75,7 @@ const trendDocs: ScoreTrendDoc[] = [ describe('getTrendsFromQueryResult', () => { it('should return value matching Trends type definition, in descending order, and with postureScore', async () => { - const trends = getTrendsFromQueryResult(trendDocs); + const trends = formatTrends(trendDocs); expect(trends).toEqual([ { timestamp: '2022-04-06T15:30:00Z', @@ -79,6 +93,14 @@ describe('getTrendsFromQueryResult', () => { postureScore: 25.0, }, }, + benchmarks: { + 'cis_gcp;v2.0.0': { + totalFailed: 3, + totalFindings: 6, + totalPassed: 3, + postureScore: 50, + }, + }, }, { timestamp: '2022-04-06T15:00:00Z', @@ -102,27 +124,24 @@ describe('getTrendsFromQueryResult', () => { postureScore: 75.0, }, }, - }, - { - timestamp: '2022-04-05T15:30:00Z', - summary: { - totalFindings: 30, - totalPassed: 25, - totalFailed: 5, - postureScore: 83.3, - }, - clusters: { - forth_cluster_id: { - totalFindings: 25, - totalPassed: 25, - totalFailed: 0, - postureScore: 100.0, + benchmarks: { + 'cis_gcp;v2.0.0': { + totalFailed: 3, + totalFindings: 6, + totalPassed: 3, + postureScore: 50.0, }, - fifth_cluster_id: { - totalFindings: 5, - totalPassed: 0, - totalFailed: 5, - postureScore: 0, + 'cis_azure;v2.0.0': { + totalFailed: 3, + totalFindings: 6, + totalPassed: 3, + postureScore: 50.0, + }, + 'cis_aws;v1.5.0': { + totalFailed: 3, + totalFindings: 6, + totalPassed: 3, + postureScore: 50.0, }, }, }, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts index cc8234fa6d7af..00acd14d960fa 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts @@ -5,36 +5,49 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { calculatePostureScore } from '../../../common/utils/helpers'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS } from '../../../common/constants'; import type { PosturePolicyTemplate, Stats } from '../../../common/types'; +import { toBenchmarkDocFieldKey } from '../../lib/mapping_field_util'; +import { CSPM_FINDINGS_STATS_INTERVAL } from '../../tasks/findings_stats_task'; + +interface FindingsDetails { + total_findings: number; + passed_findings: number; + failed_findings: number; +} + +interface ScoreByClusterId { + [clusterId: string]: FindingsDetails; +} + +interface ScoreByBenchmarkId { + [benchmarkId: string]: { + [key: string]: FindingsDetails; + }; +} export interface ScoreTrendDoc { '@timestamp': string; total_findings: number; passed_findings: number; failed_findings: number; - score_by_cluster_id: Record< - string, - { - total_findings: number; - passed_findings: number; - failed_findings: number; - } - >; + score_by_cluster_id: ScoreByClusterId; + score_by_benchmark_id: ScoreByBenchmarkId; } export type Trends = Array<{ timestamp: string; summary: Stats; clusters: Record; + benchmarks: Record; }>; export const getTrendsQuery = (policyTemplate: PosturePolicyTemplate) => ({ index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, - // large number that should be sufficient for 24 hours considering we write to the score index every 5 minutes - size: 999, + // Amount of samples of the last 24 hours (accounting that we take a sample every 5 minutes) + size: (24 * 60) / CSPM_FINDINGS_STATS_INTERVAL, sort: '@timestamp:desc', query: { bool: { @@ -51,40 +64,71 @@ export const getTrendsQuery = (policyTemplate: PosturePolicyTemplate) => ({ }, }); -export const getTrendsFromQueryResult = (scoreTrendDocs: ScoreTrendDoc[]): Trends => - scoreTrendDocs.map((data) => ({ - timestamp: data['@timestamp'], - summary: { - totalFindings: data.total_findings, - totalFailed: data.failed_findings, - totalPassed: data.passed_findings, - postureScore: calculatePostureScore(data.passed_findings, data.failed_findings), - }, - clusters: Object.fromEntries( - Object.entries(data.score_by_cluster_id).map(([clusterId, cluster]) => [ - clusterId, - { - totalFindings: cluster.total_findings, - totalFailed: cluster.failed_findings, - totalPassed: cluster.passed_findings, - postureScore: calculatePostureScore(cluster.passed_findings, cluster.failed_findings), - }, - ]) - ), - })); +export const formatTrends = (scoreTrendDocs: ScoreTrendDoc[]): Trends => { + return scoreTrendDocs.map((data) => { + return { + timestamp: data['@timestamp'], + summary: { + totalFindings: data.total_findings, + totalFailed: data.failed_findings, + totalPassed: data.passed_findings, + postureScore: calculatePostureScore(data.passed_findings, data.failed_findings), + }, + clusters: Object.fromEntries( + Object.entries(data.score_by_cluster_id).map(([clusterId, cluster]) => [ + clusterId, + { + totalFindings: cluster.total_findings, + totalFailed: cluster.failed_findings, + totalPassed: cluster.passed_findings, + postureScore: calculatePostureScore(cluster.passed_findings, cluster.failed_findings), + }, + ]) + ), + benchmarks: data.score_by_benchmark_id + ? Object.fromEntries( + Object.entries(data.score_by_benchmark_id).flatMap(([benchmarkId, benchmark]) => + Object.entries(benchmark).map(([benchmarkVersion, benchmarkStats]) => { + const benchmarkIdVersion = toBenchmarkDocFieldKey(benchmarkId, benchmarkVersion); + return [ + benchmarkIdVersion, + { + totalFindings: benchmarkStats.total_findings, + totalFailed: benchmarkStats.failed_findings, + totalPassed: benchmarkStats.passed_findings, + postureScore: calculatePostureScore( + benchmarkStats.passed_findings, + benchmarkStats.failed_findings + ), + }, + ]; + }) + ) + ) + : {}, + }; + }); +}; export const getTrends = async ( esClient: ElasticsearchClient, - policyTemplate: PosturePolicyTemplate + policyTemplate: PosturePolicyTemplate, + logger: Logger ): Promise => { - const trendsQueryResult = await esClient.search(getTrendsQuery(policyTemplate)); + try { + const trendsQueryResult = await esClient.search(getTrendsQuery(policyTemplate)); - if (!trendsQueryResult.hits.hits) throw new Error('missing trend results from score index'); + if (!trendsQueryResult.hits.hits) throw new Error('missing trend results from score index'); - const scoreTrendDocs = trendsQueryResult.hits.hits.map((hit) => { - if (!hit._source) throw new Error('missing _source data for one or more of trend results'); - return hit._source; - }); + const scoreTrendDocs = trendsQueryResult.hits.hits.map((hit) => { + if (!hit._source) throw new Error('missing _source data for one or more of trend results'); + return hit._source; + }); - return getTrendsFromQueryResult(scoreTrendDocs); + return formatTrends(scoreTrendDocs); + } catch (err) { + logger.error(`Failed to fetch trendlines data ${err.message}`); + logger.error(err); + throw err; + } }; diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts index f40ce3f7dc4ab..c157e8081546a 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts @@ -32,10 +32,11 @@ import { type LatestTaskStateSchema, type TaskHealthStatus, } from './task_state'; +import { toBenchmarkMappingFieldKey } from '../lib/mapping_field_util'; const CSPM_FINDINGS_STATS_TASK_ID = 'cloud_security_posture-findings_stats'; const CSPM_FINDINGS_STATS_TASK_TYPE = 'cloud_security_posture-stats_task'; -const CSPM_FINDINGS_STATS_INTERVAL = '5m'; +export const CSPM_FINDINGS_STATS_INTERVAL = 5; export async function scheduleFindingsStatsTask( taskManager: TaskManagerStartContract, @@ -47,7 +48,7 @@ export async function scheduleFindingsStatsTask( id: CSPM_FINDINGS_STATS_TASK_ID, taskType: CSPM_FINDINGS_STATS_TASK_TYPE, schedule: { - interval: CSPM_FINDINGS_STATS_INTERVAL, + interval: `${CSPM_FINDINGS_STATS_INTERVAL}m`, }, state: emptyState, params: {}, @@ -177,6 +178,39 @@ const getScoreQuery = (): SearchRequest => ({ }, }, }, + score_by_benchmark_id: { + terms: { + field: 'rule.benchmark.id', + }, + aggregations: { + benchmark_versions: { + terms: { + field: 'rule.benchmark.version', + }, + aggs: { + total_findings: { + value_count: { + field: 'result.evaluation', + }, + }, + passed_findings: { + filter: { + term: { + 'result.evaluation': 'passed', + }, + }, + }, + failed_findings: { + filter: { + term: { + 'result.evaluation': 'failed', + }, + }, + }, + }, + }, + }, + }, }, }, }, @@ -255,6 +289,27 @@ const getFindingsScoresDocIndexingPromises = ( ]; }) ); + // creating score per benchmark id and version + const benchmarkStats = Object.fromEntries( + policyTemplateTrend.score_by_benchmark_id.buckets.map((benchmarkIdBucket) => { + const benchmarkId = benchmarkIdBucket.key; + const benchmarkVersions = Object.fromEntries( + benchmarkIdBucket.benchmark_versions.buckets.map((benchmarkVersionBucket) => { + const benchmarkVersion = toBenchmarkMappingFieldKey(benchmarkVersionBucket.key); + return [ + benchmarkVersion, + { + total_findings: benchmarkVersionBucket.total_findings.value, + passed_findings: benchmarkVersionBucket.passed_findings.doc_count, + failed_findings: benchmarkVersionBucket.failed_findings.doc_count, + }, + ]; + }) + ); + + return [benchmarkId, benchmarkVersions]; + }) + ); // each document contains the policy template and its scores return esClient.index({ @@ -265,6 +320,7 @@ const getFindingsScoresDocIndexingPromises = ( failed_findings: policyTemplateTrend.failed_findings.doc_count, total_findings: policyTemplateTrend.total_findings.value, score_by_cluster_id: clustersStats, + score_by_benchmark_id: benchmarkStats, }, }); }); diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/types.ts b/x-pack/plugins/cloud_security_posture/server/tasks/types.ts index 56ca619dcec55..839d4823ca47a 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/types.ts @@ -23,6 +23,20 @@ export interface ScoreByPolicyTemplateBucket { total_findings: { value: number }; }>; }; + score_by_benchmark_id: { + buckets: Array<{ + key: string; // benchmark id + doc_count: number; + benchmark_versions: { + buckets: Array<{ + key: string; // benchmark version + passed_findings: { doc_count: number }; + failed_findings: { doc_count: number }; + total_findings: { value: number }; + }>; + }; + }>; + }; }>; }; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2a45559b22b5e..b3c42373d85f0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12081,11 +12081,6 @@ "xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.description": "Utilisez notre intégration {integrationFullName} (KSPM) pour détecter les erreurs de configuration de sécurité dans vos clusters Kubernetes.", "xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptDescription": "Détectez et corrigez les risques de configuration potentiels dans votre infrastructure cloud, comme les compartiments S3 accessibles au public, avec nos solutions de gestion du niveau de sécurité Kubernetes et de gestion du niveau de sécurité du cloud. {learnMore}", "xpack.csp.complianceScoreBar.tooltipTitle": "{failed} résultats en échec et {passed} ayant réussi", - "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {assetId}", - "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {assetId}", - "xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "Dernière évaluation {dateFromNow}", - "xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle": "Afficher tous les échecs des résultats pour ce {postureAsset}", - "xpack.csp.dashboard.summarySection.postureScorePanelTitle": "Score de sécurité {type} global", "xpack.csp.eksIntegration.docsLink": "Lisez {docs} pour en savoir plus", "xpack.csp.findings..bottomBarLabel": "Voici les {maxItems} premiers résultats correspondant à votre recherche. Veuillez l'affiner pour en voir davantage.", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "Affichage de {pageStart}-{pageEnd} sur {total} {type}", @@ -12202,7 +12197,6 @@ "xpack.csp.cnvmDashboardTable.section.topVulnerableResources.column.vulnerabilities": "Vulnérabilités", "xpack.csp.cnvmDashboardTable.section.topVulnerableResources.column.vulnerabilityCount": "Vulnérabilités", "xpack.csp.compactFormattedNumber.naTitle": "S. O.", - "xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip": "Échec des résultats", "xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip": "Réussite des résultats", "xpack.csp.createDetectionRuleButton": "Créer une règle de détection", "xpack.csp.createPackagePolicy.customAssetsTab.cloudNativeVulnerabilityManagementTitleLabel": "Gestion des vulnérabilités natives du cloud ", @@ -12222,13 +12216,7 @@ "xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP", "xpack.csp.cspmIntegration.integration.nameTitle": "Gestion du niveau de sécurité du cloud", "xpack.csp.cspmIntegration.integration.shortNameTitle": "CSPM", - "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterPrefixTitle": "Afficher tous les résultats pour ", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.accountNameTitle": "Nom du compte", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.clusterNameTitle": "Nom du cluster", "xpack.csp.dashboard.benchmarkSection.columnsHeader.complianceByCisSectionTitle": "Conformité par section CIS", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.postureScoreTitle": "Score du niveau", - "xpack.csp.dashboard.benchmarkSection.defaultClusterTitle": "ID", - "xpack.csp.dashboard.benchmarkSection.manageRulesButton": "Gérer les règles", "xpack.csp.dashboard.cspPageTemplate.pageTitle": "Niveau de sécurité du cloud", "xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "Section CIS", "xpack.csp.dashboard.risksTable.complianceColumnLabel": "Conformité", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5357b3a0908ad..4233d3d7e76b6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12095,11 +12095,6 @@ "xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.description": "{integrationFullName}(CSPM)統合を使用して、Kubernetesクラスターの構成エラーを検出します。", "xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptDescription": "クラウドおよびKubernetesセキュリティ態勢管理ソリューションを利用して、クラウドインフラの構成リスク(誰でもアクセス可能なS3バケットなど)の可能性を検出し、修正します。{learnMore}", "xpack.csp.complianceScoreBar.tooltipTitle": "{failed}が失敗し、{passed}が調査結果に合格しました", - "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {assetId}", - "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {assetId}", - "xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "前回評価:{dateFromNow}", - "xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle": "この{postureAsset}の失敗した調査結果をすべて表示", - "xpack.csp.dashboard.summarySection.postureScorePanelTitle": "全体的な{type}態勢スコア", "xpack.csp.eksIntegration.docsLink": "詳細は{docs}をご覧ください", "xpack.csp.findings..bottomBarLabel": "これらは検索条件に一致した初めの{maxItems}件の調査結果です。他の結果を表示するには検索条件を絞ってください。", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "{total} {type}ページ中{pageStart}-{pageEnd}ページを表示中", @@ -12216,7 +12211,6 @@ "xpack.csp.cnvmDashboardTable.section.topVulnerableResources.column.vulnerabilities": "脆弱性", "xpack.csp.cnvmDashboardTable.section.topVulnerableResources.column.vulnerabilityCount": "脆弱性", "xpack.csp.compactFormattedNumber.naTitle": "N/A", - "xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip": "失敗した調査結果", "xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip": "合格した調査結果", "xpack.csp.createDetectionRuleButton": "検出ルールを作成", "xpack.csp.createPackagePolicy.customAssetsTab.cloudNativeVulnerabilityManagementTitleLabel": "Cloud Native Vulnerability Management ", @@ -12236,13 +12230,7 @@ "xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP", "xpack.csp.cspmIntegration.integration.nameTitle": "クラウドセキュリティ態勢管理", "xpack.csp.cspmIntegration.integration.shortNameTitle": "CSPM", - "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterPrefixTitle": "すべての調査結果を表示 ", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.accountNameTitle": "アカウント名", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.clusterNameTitle": "クラスター名", "xpack.csp.dashboard.benchmarkSection.columnsHeader.complianceByCisSectionTitle": "CISセクション別のコンプライアンス", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.postureScoreTitle": "態勢スコア", - "xpack.csp.dashboard.benchmarkSection.defaultClusterTitle": "ID", - "xpack.csp.dashboard.benchmarkSection.manageRulesButton": "ルールの管理", "xpack.csp.dashboard.cspPageTemplate.pageTitle": "クラウドセキュリティ態勢", "xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CISセクション", "xpack.csp.dashboard.risksTable.complianceColumnLabel": "コンプライアンス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 916c2eba4d8e9..8fed92fd84221 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12095,11 +12095,6 @@ "xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.description": "使用我们的 {integrationFullName} (KSPM) 集成可在您的 Kubernetes 集群中检测安全配置错误。", "xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptDescription": "使用我们的云和 Kubernetes 安全态势管理解决方案,在您的云基础设施中检测并缓解潜在的配置风险,如可公开访问的 S3 存储桶。{learnMore}", "xpack.csp.complianceScoreBar.tooltipTitle": "{failed} 个失败和 {passed} 个通过的结果", - "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {assetId}", - "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {assetId}", - "xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "上次评估于 {dateFromNow}", - "xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle": "查看此 {postureAsset} 的所有失败结果", - "xpack.csp.dashboard.summarySection.postureScorePanelTitle": "总体 {type} 态势分数", "xpack.csp.eksIntegration.docsLink": "请参阅 {docs} 了解更多详情", "xpack.csp.findings..bottomBarLabel": "这些是匹配您的搜索的前 {maxItems} 个结果,请优化搜索以查看其他结果。", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "正在显示第 {pageStart}-{pageEnd} 个 {type}(共 {total} 个)", @@ -12216,7 +12211,6 @@ "xpack.csp.cnvmDashboardTable.section.topVulnerableResources.column.vulnerabilities": "漏洞", "xpack.csp.cnvmDashboardTable.section.topVulnerableResources.column.vulnerabilityCount": "漏洞", "xpack.csp.compactFormattedNumber.naTitle": "不可用", - "xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip": "失败的结果", "xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip": "通过的结果", "xpack.csp.createDetectionRuleButton": "创建检测规则", "xpack.csp.createPackagePolicy.customAssetsTab.cloudNativeVulnerabilityManagementTitleLabel": "云原生漏洞管理 ", @@ -12236,13 +12230,7 @@ "xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP", "xpack.csp.cspmIntegration.integration.nameTitle": "云安全态势管理", "xpack.csp.cspmIntegration.integration.shortNameTitle": "CSPM", - "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterPrefixTitle": "显示以下所有结果 ", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.accountNameTitle": "帐户名称", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.clusterNameTitle": "集群名称", "xpack.csp.dashboard.benchmarkSection.columnsHeader.complianceByCisSectionTitle": "合规性(按 CIS 部分)", - "xpack.csp.dashboard.benchmarkSection.columnsHeader.postureScoreTitle": "态势分数", - "xpack.csp.dashboard.benchmarkSection.defaultClusterTitle": "ID", - "xpack.csp.dashboard.benchmarkSection.manageRulesButton": "管理规则", "xpack.csp.dashboard.cspPageTemplate.pageTitle": "云安全态势", "xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CIS 部分", "xpack.csp.dashboard.risksTable.complianceColumnLabel": "合规性", diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index a206fd563cc00..e7a34bafe9fb5 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -17,6 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./telemetry/telemetry.ts'), require.resolve('./routes/vulnerabilities_dashboard.ts'), + require.resolve('./routes/stats.ts'), ], junit: { reportName: 'X-Pack Cloud Security Posture API Tests', diff --git a/x-pack/test/cloud_security_posture_api/routes/mocks/benchmark_score_mock.ts b/x-pack/test/cloud_security_posture_api/routes/mocks/benchmark_score_mock.ts new file mode 100644 index 0000000000000..f24c960783e53 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/mocks/benchmark_score_mock.ts @@ -0,0 +1,300 @@ +/* + * 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. + */ + +export const getBenchmarkScoreMockData = (postureType: string) => [ + { + total_findings: 1, + policy_template: postureType, + '@timestamp': '2023-11-22T16:10:55.229268215Z', + score_by_cluster_id: { + 'Another Upper case account id': { + total_findings: 1, + passed_findings: 1, + failed_findings: 0, + }, + 'Upper case cluster id': { + total_findings: 1, + passed_findings: 1, + failed_findings: 0, + }, + }, + score_by_benchmark_id: { + cis_aws: { + v1_5_0: { + total_findings: 1, + passed_findings: 1, + failed_findings: 0, + }, + }, + cis_k8s: { + v1_0_0: { + total_findings: 1, + passed_findings: 1, + failed_findings: 0, + }, + }, + }, + passed_findings: 1, + failed_findings: 0, + }, +]; + +export const cspmComplianceDashboardDataMockV1 = { + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + resourcesEvaluated: 1, + }, + groupedFindingsEvaluation: [ + { + name: 'Another upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + clusters: [ + { + meta: { + clusterId: 'Another Upper case account id', + assetIdentifierId: 'Another Upper case account id', + benchmark: { + name: 'CIS AWS2', + id: 'cis_aws', + posture_type: 'cspm', + version: 'v1.5.0', + }, + cloud: { + account: { + id: 'Another Upper case account id', + }, + }, + }, + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + }, + groupedFindingsEvaluation: [ + { + name: 'Another upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], +}; + +export const cspmComplianceDashboardDataMockV2 = { + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + resourcesEvaluated: 1, + }, + groupedFindingsEvaluation: [ + { + name: 'Another upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + benchmarks: [ + { + meta: { + benchmarkId: 'cis_aws', + benchmarkVersion: 'v1.5.0', + benchmarkName: 'CIS AWS2', + assetCount: 1, + }, + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + }, + groupedFindingsEvaluation: [ + { + name: 'Another upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], +}; + +export const kspmComplianceDashboardDataMockV1 = { + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + resourcesEvaluated: 1, + }, + groupedFindingsEvaluation: [ + { + name: 'Upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + clusters: [ + { + meta: { + clusterId: 'Upper case cluster id', + assetIdentifierId: 'Upper case cluster id', + benchmark: { + name: 'CIS Kubernetes V1.23', + id: 'cis_k8s', + posture_type: 'kspm', + version: 'v1.0.0', + }, + cluster: { + id: 'Upper case cluster id', + }, + }, + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + }, + groupedFindingsEvaluation: [ + { + name: 'Upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], +}; + +export const kspmComplianceDashboardDataMockV2 = { + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + resourcesEvaluated: 1, + }, + groupedFindingsEvaluation: [ + { + name: 'Upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + benchmarks: [ + { + meta: { + benchmarkId: 'cis_k8s', + benchmarkVersion: 'v1.0.0', + benchmarkName: 'CIS Kubernetes V1.23', + assetCount: 1, + }, + stats: { + totalFailed: 0, + totalPassed: 1, + totalFindings: 1, + postureScore: 100, + }, + groupedFindingsEvaluation: [ + { + name: 'Upper case section', + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], + }, + ], + trend: [ + { + totalFindings: 1, + totalFailed: 0, + totalPassed: 1, + postureScore: 100, + }, + ], +}; diff --git a/x-pack/test/cloud_security_posture_api/routes/mocks/findings_mock.ts b/x-pack/test/cloud_security_posture_api/routes/mocks/findings_mock.ts new file mode 100644 index 0000000000000..92ca03b1e4789 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/mocks/findings_mock.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 Chance from 'chance'; + +const chance = new Chance(); + +export const findingsMockData = [ + { + '@timestamp': '2023-06-29T02:08:44.993Z', + resource: { + id: chance.guid(), + name: `kubelet`, + sub_type: 'lower case sub type', + type: 'k8s_resource_type', + }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + }, + orchestrator: { + cluster: { id: 'Upper case cluster id' }, + }, + }, + { + '@timestamp': '2023-06-29T02:08:44.993Z', + resource: { + id: chance.guid(), + name: `Pod`, + sub_type: 'Upper case sub type', + type: 'cloud_resource_type', + }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + rule: { + name: 'lower case rule name', + section: 'Another upper case section', + benchmark: { + id: 'cis_aws', + posture_type: 'cspm', + name: 'CIS AWS2', + version: 'v1.5.0', + }, + }, + cloud: { + account: { id: 'Another Upper case account id' }, + }, + }, +]; diff --git a/x-pack/test/cloud_security_posture_api/routes/stats.ts b/x-pack/test/cloud_security_posture_api/routes/stats.ts new file mode 100644 index 0000000000000..92dac0d6b0277 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/stats.ts @@ -0,0 +1,229 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + BENCHMARK_SCORE_INDEX_DEFAULT_NS, + LATEST_FINDINGS_INDEX_DEFAULT_NS, +} from '@kbn/cloud-security-posture-plugin/common/constants'; +import { + BenchmarkData, + Cluster, + ComplianceDashboardData, + ComplianceDashboardDataV2, + PostureTrend, +} from '@kbn/cloud-security-posture-plugin/common/types'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { + getBenchmarkScoreMockData, + kspmComplianceDashboardDataMockV1, + kspmComplianceDashboardDataMockV2, + cspmComplianceDashboardDataMockV1, + cspmComplianceDashboardDataMockV2, +} from './mocks/benchmark_score_mock'; +import { findingsMockData } from './mocks/findings_mock'; + +const removeRealtimeCalculatedFields = (trends: PostureTrend[]) => { + return trends.map((trend: PostureTrend) => { + const { timestamp, ...rest } = trend; + return rest; + }); +}; + +const removeRealtimeClusterFields = (clusters: Cluster[]) => + clusters.flatMap((cluster) => { + const clusterWithoutTrend = { + ...cluster, + trend: removeRealtimeCalculatedFields(cluster.trend), + }; + const { lastUpdate, ...clusterWithoutTime } = clusterWithoutTrend.meta; + + return { ...clusterWithoutTrend, meta: clusterWithoutTime }; + }); + +const removeRealtimeBenchmarkFields = (benchmarks: BenchmarkData[]) => + benchmarks.flatMap((benchmark) => ({ + ...benchmark, + trend: removeRealtimeCalculatedFields(benchmark.trend), + })); + +// eslint-disable-next-line import/no-default-export +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + + const kibanaHttpClient = getService('supertest'); + + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const index = { + addFindings: async (findingsMock: T[]) => { + await Promise.all( + findingsMock.map((findingsDoc) => + es.index({ + index: LATEST_FINDINGS_INDEX_DEFAULT_NS, + body: { ...findingsDoc, '@timestamp': new Date().toISOString() }, + refresh: true, + }) + ) + ); + }, + + addScores: async (scoresMock: T[]) => { + await Promise.all( + scoresMock.map((scoreDoc) => + es.index({ + index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, + body: { ...scoreDoc, '@timestamp': new Date().toISOString() }, + refresh: true, + }) + ) + ); + }, + + removeFindings: async () => { + const indexExists = await es.indices.exists({ index: LATEST_FINDINGS_INDEX_DEFAULT_NS }); + + if (indexExists) { + es.deleteByQuery({ + index: LATEST_FINDINGS_INDEX_DEFAULT_NS, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + removeScores: async () => { + const indexExists = await es.indices.exists({ index: BENCHMARK_SCORE_INDEX_DEFAULT_NS }); + + if (indexExists) { + es.deleteByQuery({ + index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + deleteFindingsIndex: async () => { + const indexExists = await es.indices.exists({ index: LATEST_FINDINGS_INDEX_DEFAULT_NS }); + + if (indexExists) { + await es.indices.delete({ index: LATEST_FINDINGS_INDEX_DEFAULT_NS }); + } + }, + }; + + describe('GET /internal/cloud_security_posture/stats', () => { + describe('CSPM Compliance Dashboard Stats API', async () => { + beforeEach(async () => { + await index.removeFindings(); + await index.removeScores(); + + await waitForPluginInitialized(); + await index.addScores(getBenchmarkScoreMockData('cspm')); + await index.addFindings([findingsMockData[1]]); + }); + + it('should return CSPM cluster V1 ', async () => { + const { body: res }: { body: ComplianceDashboardData } = await kibanaHttpClient + .get(`/internal/cloud_security_posture/stats/cspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .expect(200); + const resClusters = removeRealtimeClusterFields(res.clusters); + const trends = removeRealtimeCalculatedFields(res.trend); + + expect({ + ...res, + clusters: resClusters, + trend: trends, + }).to.eql(cspmComplianceDashboardDataMockV1); + }); + + it('should return CSPM benchmarks V2 ', async () => { + const { body: res }: { body: ComplianceDashboardDataV2 } = await kibanaHttpClient + .get(`/internal/cloud_security_posture/stats/cspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const resBenchmarks = removeRealtimeBenchmarkFields(res.benchmarks); + + const trends = removeRealtimeCalculatedFields(res.trend); + + expect({ + ...res, + benchmarks: resBenchmarks, + trend: trends, + }).to.eql(cspmComplianceDashboardDataMockV2); + }); + }); + + describe('KSPM Compliance Dashboard Stats API', async () => { + beforeEach(async () => { + await index.removeFindings(); + await index.removeScores(); + + await waitForPluginInitialized(); + await index.addScores(getBenchmarkScoreMockData('kspm')); + await index.addFindings([findingsMockData[0]]); + }); + + it('should return KSPM clusters V1 ', async () => { + const { body: res }: { body: ComplianceDashboardData } = await kibanaHttpClient + .get(`/internal/cloud_security_posture/stats/kspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const resClusters = removeRealtimeClusterFields(res.clusters); + const trends = removeRealtimeCalculatedFields(res.trend); + + expect({ + ...res, + clusters: resClusters, + trend: trends, + }).to.eql(kspmComplianceDashboardDataMockV1); + }); + + it('should return KSPM benchmarks V2 ', async () => { + const { body: res }: { body: ComplianceDashboardDataV2 } = await kibanaHttpClient + .get(`/internal/cloud_security_posture/stats/kspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const resBenchmarks = removeRealtimeBenchmarkFields(res.benchmarks); + + const trends = removeRealtimeCalculatedFields(res.trend); + + expect({ + ...res, + benchmarks: resBenchmarks, + trend: trends, + }).to.eql(kspmComplianceDashboardDataMockV2); + }); + }); + }); +}