Skip to content

Commit

Permalink
Compliance dashboard UI and API (#171312)
Browse files Browse the repository at this point in the history
## Summary

Summarize your PR. If it involves visual changes include a screenshot or
gif.

- Add benchmarks to Compliance Dashboard API.
- Add `score_by_benchmark_id` to the Benchmark Scores Index this will
show posture stats for each benchmark id
- Add  benchmark aggregation query using  benchmark id and version
- Add BWC API versioning 
- STATS API V1 should show clusters 
- STATS API V2 should show benchmarks 
- Add unit tests
- Added integration tests with API versioning test cases.


To test PR with API versioning, in Kibana client -
`x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts`
- Change version value to 1 to see clusters 
```
http.get<ComplianceDashboardData>(getStatsRoute(CSPM_POLICY_TEMPLATE), { version: '1' }),
```
- Change version value to 2 to see versions
```
http.get<ComplianceDashboardData>(getStatsRoute(CSPM_POLICY_TEMPLATE), { version: '2' })
```

<img width="1721" alt="image"
src="https://github.com/elastic/kibana/assets/17135495/7fb53dec-c405-49e5-aa22-7788b4d1d5c0">

Uploading Untitled 2.mov…
  • Loading branch information
Omolola-Akinleye authored Nov 30, 2023
1 parent ab5ff9c commit af28af8
Show file tree
Hide file tree
Showing 34 changed files with 1,779 additions and 462 deletions.
19 changes: 19 additions & 0 deletions x-pack/plugins/cloud_security_posture/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,32 @@ 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[];
clusters: Cluster[];
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,23 +23,25 @@ export const getStatsRoute = (policyTemplate: PosturePolicyTemplate) => {
};

export const useCspmStatsApi = (
options: UseQueryOptions<unknown, unknown, ComplianceDashboardData, string[]>
options: UseQueryOptions<unknown, unknown, ComplianceDashboardDataV2, string[]>
) => {
const { http } = useKibana().services;
return useQuery(
getCspmStatsKey,
() => http.get<ComplianceDashboardData>(getStatsRoute(CSPM_POLICY_TEMPLATE), { version: '1' }),
() =>
http.get<ComplianceDashboardDataV2>(getStatsRoute(CSPM_POLICY_TEMPLATE), { version: '2' }),
options
);
};

export const useKspmStatsApi = (
options: UseQueryOptions<unknown, unknown, ComplianceDashboardData, string[]>
options: UseQueryOptions<unknown, unknown, ComplianceDashboardDataV2, string[]>
) => {
const { http } = useKibana().services;
return useQuery(
getKspmStatsKey,
() => http.get<ComplianceDashboardData>(getStatsRoute(KSPM_POLICY_TEMPLATE), { version: '1' }),
() =>
http.get<ComplianceDashboardDataV2>(getStatsRoute(KSPM_POLICY_TEMPLATE), { version: '2' }),
options
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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();
Expand All @@ -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 && (
<EuiFlexItem
key={benchmark.type}
onClick={() => {
Expand Down Expand Up @@ -98,7 +98,7 @@ export const AccountsEvaluatedWidget = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CompactFormattedNumber
number={clusterAmount}
number={cloudAssetAmount}
abbreviateAbove={benchmarkAbbreviateAbove}
/>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface ChartPanelProps {
isLoading?: boolean;
isError?: boolean;
rightSideItems?: ReactNode[];
styles?: React.CSSProperties;
}

const Loading = () => (
Expand Down Expand Up @@ -54,6 +55,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
isError,
children,
rightSideItems,
styles,
}) => {
const { euiTheme } = useEuiTheme();
const renderChart = () => {
Expand All @@ -63,7 +65,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
};

return (
<EuiPanel hasBorder={hasBorder} hasShadow={false} data-test-subj="chart-panel">
<EuiPanel hasBorder={hasBorder} hasShadow={false} style={styles} data-test-subj="chart-panel">
<EuiFlexGroup direction="column" gutterSize="m" style={{ height: '100%' }}>
<EuiFlexItem grow={false}>
<EuiFlexGroup justifyContent={'spaceBetween'}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
<>
<EuiText
size="s"
style={{
fontWeight: euiTheme.font.weight.bold,
marginBottom: euiTheme.size.xs,
}}
>
{text}
</EuiText>

<EuiLink
color="text"
onClick={onClick}
css={css`
display: flex;
&:hover {
text-decoration: none;
}
`}
>
<EuiText
color={color}
css={css`
&:hover {
border-bottom: 2px solid ${color};
padding-bottom: 4px;
}
`}
style={{ fontWeight: euiTheme.font.weight.medium, fontSize: '18px' }}
size="s"
>
<CompactFormattedNumber number={count} abbreviateAbove={999} />
&nbsp;
</EuiText>
</EuiLink>
</>
);
};

const CompactPercentageLabels = ({
onEvalCounterClick,
stats,
}: {
onEvalCounterClick: (evaluation: Evaluation) => void;
stats: { totalPassed: number; totalFailed: number };
}) => (
<>
<CounterLink
text="passed"
count={stats.totalPassed}
color={statusColors.passed}
onClick={() => onEvalCounterClick(RULE_PASSED)}
tooltipContent={i18n.translate(
'xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip',
{ defaultMessage: 'Passed findings' }
)}
/>
<EuiText size="s">&nbsp;-&nbsp;</EuiText>
<CounterLink
text="failed"
count={stats.totalFailed}
color={statusColors.failed}
onClick={() => 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 (
<EuiFlexGroup gutterSize="l" justifyContent="spaceBetween">
<EuiFlexItem grow={false} style={borderLeftStyles}>
<CounterButtonLink
text="Passed Findings"
count={stats.totalPassed}
color={statusColors.passed}
onClick={() => onEvalCounterClick(RULE_PASSED)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={borderLeftStyles}>
<CounterButtonLink
text="Failed Findings"
count={stats.totalFailed}
color={statusColors.failed}
onClick={() => onEvalCounterClick(RULE_FAILED)}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

const getPostureScorePercentage = (postureScore: number): string => `${Math.round(postureScore)}%`;

const PercentageInfo = ({
Expand Down Expand Up @@ -177,27 +295,17 @@ export const ComplianceScoreChart = ({
alignItems="flexStart"
style={{ paddingRight: euiTheme.size.xl }}
>
<CounterLink
text="passed"
count={data.totalPassed}
color={statusColors.passed}
onClick={() => onEvalCounterClick(RULE_PASSED)}
tooltipContent={i18n.translate(
'xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip',
{ defaultMessage: 'Passed findings' }
)}
/>
<EuiText size="s">&nbsp;-&nbsp;</EuiText>
<CounterLink
text="failed"
count={data.totalFailed}
color={statusColors.failed}
onClick={() => onEvalCounterClick(RULE_FAILED)}
tooltipContent={i18n.translate(
'xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip',
{ defaultMessage: 'Failed findings' }
)}
/>
{compact ? (
<CompactPercentageLabels
stats={{ totalPassed: data.totalPassed, totalFailed: data.totalFailed }}
onEvalCounterClick={onEvalCounterClick}
/>
) : (
<NonCompactPercentageLabels
stats={{ totalPassed: data.totalPassed, totalFailed: data.totalFailed }}
onEvalCounterClick={onEvalCounterClick}
/>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Loading

0 comments on commit af28af8

Please sign in to comment.