[Cloud Posture] Compliance by CIS section table (#145114)

This commit is contained in:
Jordan 2022-11-14 23:09:32 +02:00 committed by GitHub
parent 733011b76c
commit b72a9a3df2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 166 additions and 55 deletions

View file

@ -16,10 +16,10 @@ export interface FindingsEvaluation {
totalFindings: number;
totalPassed: number;
totalFailed: number;
postureScore: Score;
}
export interface Stats extends FindingsEvaluation {
postureScore: Score;
resourcesEvaluated?: number;
}

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import { euiPaletteForStatus } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../common/constants';
const [success, warning, danger] = euiPaletteForStatus(3);
export const statusColors = {
passed: euiThemeVars.euiColorVis0,
failed: euiThemeVars.euiColorVis9,
};
export const statusColors = { success, warning, danger };
export const CSP_MOMENT_FORMAT = 'MMMM D, YYYY @ HH:mm:ss.SSS';
export type CloudPostureIntegrations = typeof cloudPostureIntegrations;

View file

@ -29,6 +29,7 @@ import {
import { FormattedDate, FormattedTime } from '@kbn/i18n-react';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { statusColors } from '../../../common/constants';
import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants';
import { CompactFormattedNumber } from '../../../components/compact_formatted_number';
import type { Evaluation, PostureTrend, Stats } from '../../../../common/types';
@ -163,7 +164,7 @@ export const CloudPostureScoreChart = ({
<CounterLink
text="passed"
count={data.totalPassed}
color="success"
color={statusColors.passed}
onClick={() => onEvalCounterClick(RULE_PASSED)}
tooltipContent={i18n.translate(
'xpack.csp.cloudPostureScoreChart.counterLink.passedFindingsTooltip',
@ -174,7 +175,7 @@ export const CloudPostureScoreChart = ({
<CounterLink
text="failed"
count={data.totalFailed}
color="danger"
color={statusColors.failed}
onClick={() => onEvalCounterClick(RULE_FAILED)}
tooltipContent={i18n.translate(
'xpack.csp.cloudPostureScoreChart.counterLink.failedFindingsTooltip',

View file

@ -12,6 +12,7 @@ const podsAgg = {
totalFindings: 2,
totalPassed: 1,
totalFailed: 1,
postureScore: 50.0,
};
const etcdAgg = {
@ -19,6 +20,7 @@ const etcdAgg = {
totalFindings: 5,
totalPassed: 0,
totalFailed: 5,
postureScore: 0,
};
const clusterAgg = {
@ -26,6 +28,7 @@ const clusterAgg = {
totalFindings: 2,
totalPassed: 2,
totalFailed: 0,
postureScore: 100.0,
};
const systemAgg = {
@ -33,6 +36,7 @@ const systemAgg = {
totalFindings: 10,
totalPassed: 6,
totalFailed: 4,
postureScore: 60.0,
};
const apiAgg = {
@ -40,6 +44,7 @@ const apiAgg = {
totalFindings: 19100,
totalPassed: 2100,
totalFailed: 17000,
postureScore: 10.9,
};
const serverAgg = {
@ -47,6 +52,7 @@ const serverAgg = {
totalFindings: 7,
totalPassed: 4,
totalFailed: 3,
postureScore: 57.1,
};
const mockData: RisksTableProps['data'] = [
@ -59,15 +65,11 @@ const mockData: RisksTableProps['data'] = [
];
describe('getTopRisks', () => {
it('returns sorted by failed findings', () => {
expect(getTopRisks([systemAgg, etcdAgg, apiAgg], 3)).toEqual([apiAgg, etcdAgg, systemAgg]);
it('returns sorted by posture score', () => {
expect(getTopRisks([systemAgg, etcdAgg, apiAgg], 3)).toEqual([etcdAgg, apiAgg, systemAgg]);
});
it('return array filtered with failed findings only', () => {
expect(getTopRisks([systemAgg, clusterAgg, apiAgg], 3)).toEqual([apiAgg, systemAgg]);
});
it('return sorted and filtered array with the correct number of elements', () => {
expect(getTopRisks(mockData, 5)).toEqual([apiAgg, etcdAgg, systemAgg, serverAgg, podsAgg]);
it('return sorted array with the correct number of elements', () => {
expect(getTopRisks(mockData, 5)).toEqual([etcdAgg, apiAgg, podsAgg, serverAgg, systemAgg]);
});
});

View file

@ -7,24 +7,26 @@
import React, { useMemo } from 'react';
import {
EuiBasicTable,
EuiBasicTableColumn,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiText,
EuiToolTip,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { statusColors } from '../../../common/constants';
import { ComplianceDashboardData, GroupedFindingsEvaluation } from '../../../../common/types';
import { CompactFormattedNumber } from '../../../components/compact_formatted_number';
export interface RisksTableProps {
data: ComplianceDashboardData['groupedFindingsEvaluation'];
maxItems: number;
onCellClick: (name: string) => void;
onViewAllClick: () => void;
viewAllButtonTitle: string;
compact?: boolean;
}
@ -32,19 +34,23 @@ export const getTopRisks = (
groupedFindingsEvaluation: ComplianceDashboardData['groupedFindingsEvaluation'],
maxItems: number
) => {
const filtered = groupedFindingsEvaluation.filter((x) => x.totalFailed > 0);
const sorted = filtered.slice().sort((first, second) => second.totalFailed - first.totalFailed);
const sorted = groupedFindingsEvaluation
.slice()
.sort((first, second) => first.postureScore - second.postureScore);
return sorted.slice(0, maxItems);
};
export const RisksTable = ({
data: resourcesTypes,
data: cisSectionsEvaluations,
maxItems,
onCellClick,
onViewAllClick,
viewAllButtonTitle,
compact,
}: RisksTableProps) => {
const { euiTheme } = useEuiTheme();
const columns: Array<EuiBasicTableColumn<GroupedFindingsEvaluation>> = useMemo(
() => [
{
@ -62,49 +68,87 @@ export const RisksTable = ({
),
},
{
field: 'totalFailed',
field: 'postureScore',
width: '115px',
name: compact
? ''
: i18n.translate('xpack.csp.dashboard.risksTable.findingsColumnLabel', {
defaultMessage: 'Findings',
: i18n.translate('xpack.csp.dashboard.risksTable.complianceColumnLabel', {
defaultMessage: 'Compliance',
}),
render: (
totalFailed: GroupedFindingsEvaluation['totalFailed'],
resource: GroupedFindingsEvaluation
) => (
<>
<EuiText size="s" color="danger">
<CompactFormattedNumber number={resource.totalFailed} />
</EuiText>
<EuiText size="s">
{'/'}
<CompactFormattedNumber number={resource.totalFindings} />
</EuiText>
</>
render: (postureScore: GroupedFindingsEvaluation['postureScore'], data) => (
<EuiFlexGroup
gutterSize="none"
alignItems="center"
justifyContent="flexEnd"
style={{ gap: euiTheme.size.s }}
>
<EuiFlexItem>
<EuiToolTip
content={i18n.translate(
'xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip',
{
defaultMessage: '{passed}/{total}',
values: { passed: data.totalPassed, total: data.totalFindings },
}
)}
>
<EuiFlexGroup
gutterSize="none"
style={{
height: euiTheme.size.xs,
borderRadius: euiTheme.border.radius.medium,
overflow: 'hidden',
gap: 1,
}}
>
<EuiFlexItem
style={{
flex: data.totalFailed,
background: statusColors.failed,
}}
/>
<EuiFlexItem
style={{
flex: data.totalPassed,
background: statusColors.passed,
}}
/>
</EuiFlexGroup>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" style={{ fontWeight: euiTheme.font.weight.bold }}>{`${
postureScore?.toFixed(0) || 0
}%`}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
),
},
],
[compact, onCellClick]
[
compact,
euiTheme.border.radius.medium,
euiTheme.font.weight.bold,
euiTheme.size.s,
euiTheme.size.xs,
onCellClick,
]
);
const items = useMemo(() => getTopRisks(resourcesTypes, maxItems), [resourcesTypes, maxItems]);
const sortedByComplianceScore = getTopRisks(cisSectionsEvaluations, maxItems);
return (
<EuiFlexGroup direction="column" justifyContent="spaceBetween" gutterSize="none">
<EuiFlexItem>
<EuiBasicTable<GroupedFindingsEvaluation>
rowHeader="name"
items={items}
<EuiInMemoryTable<GroupedFindingsEvaluation>
items={sortedByComplianceScore}
columns={columns}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
<EuiButtonEmpty onClick={onViewAllClick} iconType="search">
<FormattedMessage
id="xpack.csp.dashboard.risksTable.viewAllButtonTitle"
defaultMessage="View all failed findings"
/>
{viewAllButtonTitle}
</EuiButtonEmpty>
</div>
</EuiFlexItem>

View file

@ -43,42 +43,49 @@ export const mockDashboardData: ComplianceDashboardData = {
totalFindings: 104,
totalFailed: 0,
totalPassed: 104,
postureScore: 100,
},
{
name: 'API Server',
totalFindings: 27,
totalFailed: 11,
totalPassed: 16,
postureScore: 59.2,
},
{
name: 'Master Node Configuration Files',
totalFindings: 17,
totalFailed: 1,
totalPassed: 16,
postureScore: 94.1,
},
{
name: 'Kubelet',
totalFindings: 11,
totalFailed: 4,
totalPassed: 7,
postureScore: 63.6,
},
{
name: 'etcd',
totalFindings: 6,
totalFailed: 0,
totalPassed: 6,
postureScore: 100,
},
{
name: 'Worker Node Configuration Files',
totalFindings: 5,
totalFailed: 0,
totalPassed: 5,
postureScore: 100,
},
{
name: 'Scheduler',
totalFindings: 2,
totalFailed: 1,
totalPassed: 1,
postureScore: 50.0,
},
],
clusters: [
@ -101,42 +108,49 @@ export const mockDashboardData: ComplianceDashboardData = {
totalFindings: 104,
totalFailed: 0,
totalPassed: 104,
postureScore: 100,
},
{
name: 'API Server',
totalFindings: 27,
totalFailed: 11,
totalPassed: 16,
postureScore: 59.2,
},
{
name: 'Master Node Configuration Files',
totalFindings: 17,
totalFailed: 1,
totalPassed: 16,
postureScore: 94.1,
},
{
name: 'Kubelet',
totalFindings: 11,
totalFailed: 4,
totalPassed: 7,
postureScore: 63.6,
},
{
name: 'etcd',
totalFindings: 6,
totalFailed: 0,
totalPassed: 6,
postureScore: 100,
},
{
name: 'Worker Node Configuration Files',
totalFindings: 5,
totalFailed: 0,
totalPassed: 5,
postureScore: 100,
},
{
name: 'Scheduler',
totalFindings: 2,
totalFailed: 1,
totalPassed: 1,
postureScore: 50.0,
},
],
trend: [

View file

@ -8,6 +8,7 @@
import React from 'react';
import { EuiFlexItem, EuiFlexGroup, useEuiTheme, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CloudPostureScoreChart } from '../compliance_charts/cloud_posture_score_chart';
import type { ComplianceDashboardData, Evaluation } from '../../../../common/types';
import { RisksTable } from '../compliance_charts/risks_table';
@ -126,6 +127,10 @@ export const CloudBenchmarksSection = ({
onCellClick={(resourceTypeName) =>
handleCellClick(cluster.meta.clusterId, resourceTypeName)
}
viewAllButtonTitle={i18n.translate(
'xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle',
{ defaultMessage: 'View all failed findings for this cluster' }
)}
onViewAllClick={() => handleViewAllClick(cluster.meta.clusterId)}
/>
</div>

View file

@ -9,6 +9,7 @@ import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FlexItemGrowSize } from '@elastic/eui/src/components/flex/flex_item';
import { statusColors } from '../../../common/constants';
import { DASHBOARD_COUNTER_CARDS } from '../test_subjects';
import { CspCounterCard, CspCounterCardProps } from '../../../components/csp_counter_card';
import { CompactFormattedNumber } from '../../../components/compact_formatted_number';
@ -79,7 +80,7 @@ export const CloudSummarySection = ({
{ defaultMessage: 'Failing Findings' }
),
title: <CompactFormattedNumber number={complianceData.stats.totalFailed} />,
titleColor: complianceData.stats.totalFailed > 0 ? 'danger' : 'text',
titleColor: complianceData.stats.totalFailed > 0 ? statusColors.failed : 'text',
onClick: () => {
navToFindings({ 'result.evaluation': RULE_FAILED });
},
@ -131,6 +132,10 @@ export const CloudSummarySection = ({
maxItems={5}
onCellClick={handleCellClick}
onViewAllClick={handleViewAllClick}
viewAllButtonTitle={i18n.translate(
'xpack.csp.dashboard.risksTable.viewAllButtonTitle',
{ defaultMessage: 'View all failed findings' }
)}
/>
</ChartPanel>
</EuiFlexItem>

View file

@ -47,6 +47,9 @@ const mockClusterBuckets: ClusterBucket[] = [
passed_findings: {
doc_count: 3,
},
score: {
value: 0.5,
},
},
{
key: 'boo_type',
@ -57,6 +60,9 @@ const mockClusterBuckets: ClusterBucket[] = [
passed_findings: {
doc_count: 3,
},
score: {
value: 0.5,
},
},
],
},
@ -87,12 +93,14 @@ describe('getClustersFromAggs', () => {
totalFindings: 6,
totalFailed: 3,
totalPassed: 3,
postureScore: 50.0,
},
{
name: 'boo_type',
totalFindings: 6,
totalFailed: 3,
totalPassed: 3,
postureScore: 50.0,
},
],
},

View file

@ -17,6 +17,9 @@ const resourceTypeBuckets: FailedFindingsBucket[] = [
passed_findings: {
doc_count: 11,
},
score: {
value: 0.268,
},
},
{
key: 'boo_type',
@ -27,6 +30,9 @@ const resourceTypeBuckets: FailedFindingsBucket[] = [
passed_findings: {
doc_count: 6,
},
score: {
value: 0.545,
},
},
];
@ -39,12 +45,14 @@ describe('getFailedFindingsFromAggs', () => {
totalFindings: 41,
totalFailed: 30,
totalPassed: 11,
postureScore: 26.8,
},
{
name: 'boo_type',
totalFindings: 11,
totalFailed: 5,
totalPassed: 6,
postureScore: 54.5,
},
]);
});

View file

@ -11,6 +11,7 @@ import type {
QueryDslQueryContainer,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { calculatePostureScore } from './get_stats';
import type { ComplianceDashboardData } from '../../../common/types';
import { KeyDocCount } from './compliance_dashboard';
@ -25,12 +26,14 @@ export interface FailedFindingsBucket extends KeyDocCount {
passed_findings: {
doc_count: number;
};
score: { value: number };
}
export const failedFindingsAggQuery = {
aggs_by_resource_type: {
terms: {
field: 'rule.section',
size: 5,
},
aggs: {
failed_findings: {
@ -39,6 +42,22 @@ export const failedFindingsAggQuery = {
passed_findings: {
filter: { term: { 'result.evaluation': 'passed' } },
},
score: {
bucket_script: {
buckets_path: {
passed: 'passed_findings>_count',
failed: 'failed_findings>_count',
},
script: 'params.passed / (params.passed + params.failed)',
},
},
sort_by_score: {
bucket_sort: {
sort: {
score: 'asc' as 'asc',
},
},
},
},
},
};
@ -55,12 +74,18 @@ export const getRisksEsQuery = (query: QueryDslQueryContainer, pitId: string): S
export const getFailedFindingsFromAggs = (
queryResult: FailedFindingsBucket[]
): ComplianceDashboardData['groupedFindingsEvaluation'] =>
queryResult.map((bucket) => ({
name: bucket.key,
totalFindings: bucket.doc_count,
totalFailed: bucket.failed_findings.doc_count || 0,
totalPassed: bucket.passed_findings.doc_count || 0,
}));
queryResult.map((bucket) => {
const totalPassed = bucket.passed_findings.doc_count || 0;
const totalFailed = bucket.failed_findings.doc_count || 0;
return {
name: bucket.key,
totalFindings: bucket.doc_count,
totalFailed,
totalPassed,
postureScore: calculatePostureScore(totalPassed, totalFailed),
};
});
export const getGroupedFindingsEvaluation = async (
esClient: ElasticsearchClient,

View file

@ -9855,7 +9855,6 @@
"xpack.csp.cspSettings.rules": "Règles de sécurité du CSP - ",
"xpack.csp.dashboard.cspPageTemplate.pageTitle": "Niveau du cloud",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "Section CIS",
"xpack.csp.dashboard.risksTable.findingsColumnLabel": "Résultats",
"xpack.csp.dashboard.risksTable.viewAllButtonTitle": "Afficher tous les échecs des résultats",
"xpack.csp.dashboard.summarySection.cloudPostureScorePanelTitle": "Score du niveau du cloud",
"xpack.csp.expandColumnDescriptionLabel": "Développer",

View file

@ -9842,7 +9842,6 @@
"xpack.csp.cspSettings.rules": "CSPセキュリティルール - ",
"xpack.csp.dashboard.cspPageTemplate.pageTitle": "クラウド態勢",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CISセクション",
"xpack.csp.dashboard.risksTable.findingsColumnLabel": "調査結果",
"xpack.csp.dashboard.risksTable.viewAllButtonTitle": "すべてのフィールド調査結果を表示",
"xpack.csp.dashboard.summarySection.cloudPostureScorePanelTitle": "クラウド態勢スコア",
"xpack.csp.expandColumnDescriptionLabel": "拡張",

View file

@ -9860,7 +9860,6 @@
"xpack.csp.cspSettings.rules": "CSP 安全规则 - ",
"xpack.csp.dashboard.cspPageTemplate.pageTitle": "云态势",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CIS 部分",
"xpack.csp.dashboard.risksTable.findingsColumnLabel": "结果",
"xpack.csp.dashboard.risksTable.viewAllButtonTitle": "查看所有失败的结果",
"xpack.csp.dashboard.summarySection.cloudPostureScorePanelTitle": "云态势分数",
"xpack.csp.expandColumnDescriptionLabel": "展开",