mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Posture] CIS AWS support - changes to findings tables (#148945)
This commit is contained in:
parent
06cec01479
commit
48eb4d51d1
24 changed files with 206 additions and 175 deletions
|
@ -12,6 +12,7 @@ export const cspRuleMetadataSchema = rt.object({
|
|||
name: rt.string(),
|
||||
id: rt.string(),
|
||||
version: rt.string(),
|
||||
rule_number: rt.maybe(rt.string()),
|
||||
}),
|
||||
default_value: rt.maybe(rt.string()),
|
||||
description: rt.string(),
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
CLOUDBEAT_VANILLA,
|
||||
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
|
||||
} from '../constants';
|
||||
import { BenchmarkId } from '../types';
|
||||
import type { BenchmarkId, Score } from '../types';
|
||||
|
||||
/**
|
||||
* @example
|
||||
|
@ -72,3 +72,15 @@ export function assert(condition: any, msg?: string): asserts condition {
|
|||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value value is [0, 1] range
|
||||
*/
|
||||
export const roundScore = (value: number): Score => Number((value * 100).toFixed(1));
|
||||
|
||||
export const calculatePostureScore = (passed: number, failed: number): Score => {
|
||||
const total = passed + failed;
|
||||
if (total === 0) return total;
|
||||
|
||||
return roundScore(passed / (passed + failed));
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { calculatePostureScore } from '../../common/utils/helpers';
|
||||
import { statusColors } from '../common/constants';
|
||||
|
||||
export const ComplianceScoreBar = ({
|
||||
totalPassed,
|
||||
totalFailed,
|
||||
}: {
|
||||
totalPassed: number;
|
||||
totalFailed: number;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const complianceScore = calculatePostureScore(totalPassed, totalFailed);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
style={{ gap: euiTheme.size.s }}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.csp.complianceScoreBar.tooltipTitle', {
|
||||
defaultMessage: '{failed} failed and {passed} passed findings',
|
||||
values: {
|
||||
passed: totalPassed,
|
||||
failed: totalFailed,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
style={{
|
||||
height: euiTheme.size.xs,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
overflow: 'hidden',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{!!totalFailed && (
|
||||
<EuiFlexItem
|
||||
style={{
|
||||
flex: totalFailed,
|
||||
background: statusColors.failed,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!!totalPassed && (
|
||||
<EuiFlexItem
|
||||
style={{
|
||||
flex: totalPassed,
|
||||
background: statusColors.passed,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
size="xs"
|
||||
style={{ fontWeight: euiTheme.font.weight.bold }}
|
||||
>{`${complianceScore.toFixed(0)}%`}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -13,12 +13,9 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { statusColors } from '../../../common/constants';
|
||||
import { ComplianceScoreBar } from '../../../components/compliance_score_bar';
|
||||
import { ComplianceDashboardData, GroupedFindingsEvaluation } from '../../../../common/types';
|
||||
|
||||
export interface RisksTableProps {
|
||||
|
@ -49,8 +46,6 @@ export const RisksTable = ({
|
|||
viewAllButtonTitle,
|
||||
compact,
|
||||
}: RisksTableProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<GroupedFindingsEvaluation>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -76,63 +71,11 @@ export const RisksTable = ({
|
|||
defaultMessage: 'Compliance',
|
||||
}),
|
||||
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>
|
||||
<ComplianceScoreBar totalPassed={data.totalPassed} totalFailed={data.totalFailed} />
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
compact,
|
||||
euiTheme.border.radius.medium,
|
||||
euiTheme.font.weight.bold,
|
||||
euiTheme.size.s,
|
||||
euiTheme.size.xs,
|
||||
onCellClick,
|
||||
]
|
||||
[compact, onCellClick]
|
||||
);
|
||||
|
||||
const sortedByComplianceScore = getTopRisks(cisSectionsEvaluations, maxItems);
|
||||
|
|
|
@ -89,6 +89,7 @@ export const mockFindingsHit: CspFinding = {
|
|||
'Kubernetes provides a `default` service account which is used by cluster workloads where no specific service account is assigned to the pod. Where access to the Kubernetes API from a pod is required, a specific service account should be created for that pod, and rights granted to that service account. The default service account should be configured such that it does not provide a service account token and does not have any explicit rights assignments.\n',
|
||||
version: '1.0',
|
||||
benchmark: {
|
||||
rule_number: '1.1.1',
|
||||
name: 'CIS Kubernetes V1.23',
|
||||
id: 'cis_k8s',
|
||||
version: 'v1.0.0',
|
||||
|
|
|
@ -32,6 +32,7 @@ const getFakeFindings = (name: string): CspFinding & { id: string } => ({
|
|||
rule: {
|
||||
audit: chance.paragraph(),
|
||||
benchmark: {
|
||||
rule_number: '1.1.1',
|
||||
name: 'CIS Kubernetes',
|
||||
version: '1.6.0',
|
||||
id: 'cis_k8s',
|
||||
|
@ -140,12 +141,11 @@ describe('<FindingsTable />', () => {
|
|||
const row = data[0];
|
||||
|
||||
const columns = [
|
||||
'resource.id',
|
||||
'result.evaluation',
|
||||
'resource.sub_type',
|
||||
'resource.id',
|
||||
'resource.name',
|
||||
'resource.sub_type',
|
||||
'rule.name',
|
||||
'cluster_id',
|
||||
];
|
||||
|
||||
columns.forEach((field) => {
|
||||
|
|
|
@ -64,14 +64,13 @@ const FindingsTableComponent = ({
|
|||
] = useMemo(
|
||||
() => [
|
||||
getExpandColumn<CspFinding>({ onClick: setSelectedFinding }),
|
||||
createColumnWithFilters(baseFindingsColumns['resource.id'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['result.evaluation'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['resource.sub_type'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['resource.id'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['resource.name'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['resource.sub_type'], { onAddFilter }),
|
||||
baseFindingsColumns['rule.benchmark.rule_number'],
|
||||
createColumnWithFilters(baseFindingsColumns['rule.name'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['rule.benchmark.name'], { onAddFilter }),
|
||||
baseFindingsColumns['rule.section'],
|
||||
createColumnWithFilters(baseFindingsColumns.cluster_id, { onAddFilter }),
|
||||
baseFindingsColumns['@timestamp'],
|
||||
],
|
||||
[onAddFilter]
|
||||
|
|
|
@ -42,7 +42,7 @@ const getDefaultQuery = ({
|
|||
query,
|
||||
filters,
|
||||
pageIndex: 0,
|
||||
sortDirection: 'desc',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
|
||||
export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) => (
|
||||
|
@ -179,7 +179,7 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => {
|
|||
});
|
||||
}}
|
||||
sorting={{
|
||||
sort: { field: 'failed_findings', direction: urlQuery.sortDirection },
|
||||
sort: { field: 'compliance_score', direction: urlQuery.sortDirection },
|
||||
}}
|
||||
onAddFilter={(field, value, negate) =>
|
||||
setUrlQuery({
|
||||
|
|
|
@ -7,20 +7,19 @@
|
|||
import React from 'react';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import * as TEST_SUBJECTS from '../test_subjects';
|
||||
import { FindingsByResourceTable, formatNumber, getResourceId } from './findings_by_resource_table';
|
||||
import { FindingsByResourceTable, getResourceId } from './findings_by_resource_table';
|
||||
import type { PropsOf } from '@elastic/eui';
|
||||
import Chance from 'chance';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { TestProvider } from '../../../test/test_provider';
|
||||
import type { FindingsByResourcePage } from './use_findings_by_resource';
|
||||
import { calculatePostureScore } from '../../../../common/utils/helpers';
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
const getFakeFindingsByResource = (): FindingsByResourcePage => {
|
||||
const count = chance.integer();
|
||||
const total = chance.integer() + count + 1;
|
||||
const normalized = count / total;
|
||||
|
||||
const failed = chance.natural();
|
||||
const passed = chance.natural();
|
||||
const total = failed + passed;
|
||||
const [resourceName, resourceSubtype, ruleBenchmarkName, ...cisSections] = chance.unique(
|
||||
chance.word,
|
||||
5
|
||||
|
@ -33,9 +32,11 @@ const getFakeFindingsByResource = (): FindingsByResourcePage => {
|
|||
'resource.sub_type': resourceSubtype,
|
||||
'rule.section': cisSections,
|
||||
'rule.benchmark.name': ruleBenchmarkName,
|
||||
failed_findings: {
|
||||
count,
|
||||
normalized,
|
||||
compliance_score: passed / total,
|
||||
findings: {
|
||||
failed_findings: failed,
|
||||
passed_findings: passed,
|
||||
normalized: passed / total,
|
||||
total_findings: total,
|
||||
},
|
||||
};
|
||||
|
@ -50,7 +51,7 @@ describe('<FindingsByResourceTable />', () => {
|
|||
items: [],
|
||||
pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 },
|
||||
sorting: {
|
||||
sort: { field: 'failed_findings', direction: 'desc' },
|
||||
sort: { field: 'compliance_score', direction: 'desc' },
|
||||
},
|
||||
setTableOptions: jest.fn(),
|
||||
onAddFilter: jest.fn(),
|
||||
|
@ -75,7 +76,7 @@ describe('<FindingsByResourceTable />', () => {
|
|||
items: data,
|
||||
pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 },
|
||||
sorting: {
|
||||
sort: { field: 'failed_findings', direction: 'desc' },
|
||||
sort: { field: 'compliance_score', direction: 'desc' },
|
||||
},
|
||||
setTableOptions: jest.fn(),
|
||||
onAddFilter: jest.fn(),
|
||||
|
@ -97,10 +98,13 @@ describe('<FindingsByResourceTable />', () => {
|
|||
expect(within(row).getByText(item['resource.name'])).toBeInTheDocument();
|
||||
if (item['resource.sub_type'])
|
||||
expect(within(row).getByText(item['resource.sub_type'])).toBeInTheDocument();
|
||||
expect(within(row).getByText(item['rule.section'].join(', '))).toBeInTheDocument();
|
||||
expect(within(row).getByText(formatNumber(item.failed_findings.count))).toBeInTheDocument();
|
||||
expect(
|
||||
within(row).getByText(new RegExp(numeral(item.failed_findings.normalized).format('0%')))
|
||||
within(row).getByText(
|
||||
`${calculatePostureScore(
|
||||
item.findings.passed_findings,
|
||||
item.findings.failed_findings
|
||||
).toFixed(0)}%`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,17 +8,16 @@ import React, { useMemo } from 'react';
|
|||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiBasicTable,
|
||||
EuiTextColor,
|
||||
type EuiTableFieldDataColumnType,
|
||||
type CriteriaWithPagination,
|
||||
type Pagination,
|
||||
EuiToolTip,
|
||||
EuiBasicTableProps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { Link, generatePath } from 'react-router-dom';
|
||||
import { ComplianceScoreBar } from '../../../components/compliance_score_bar';
|
||||
import * as TEST_SUBJECTS from '../test_subjects';
|
||||
import type { FindingsByResourcePage } from './use_findings_by_resource';
|
||||
import { findingsNavigation } from '../../../common/navigation/constants';
|
||||
|
@ -31,9 +30,7 @@ import {
|
|||
export const formatNumber = (value: number) =>
|
||||
value < 1000 ? value : numeral(value).format('0.0a');
|
||||
|
||||
type Sorting = Required<
|
||||
EuiBasicTableProps<Pick<FindingsByResourcePage, 'failed_findings'>>
|
||||
>['sorting'];
|
||||
type Sorting = Required<EuiBasicTableProps<FindingsByResourcePage>>['sorting'];
|
||||
|
||||
interface Props {
|
||||
items: FindingsByResourcePage[];
|
||||
|
@ -66,9 +63,8 @@ const FindingsByResourceTableComponent = ({
|
|||
createColumnWithFilters(findingsByResourceColumns['resource.sub_type'], { onAddFilter }),
|
||||
createColumnWithFilters(findingsByResourceColumns['resource.name'], { onAddFilter }),
|
||||
createColumnWithFilters(findingsByResourceColumns['rule.benchmark.name'], { onAddFilter }),
|
||||
findingsByResourceColumns['rule.section'],
|
||||
createColumnWithFilters(findingsByResourceColumns.cluster_id, { onAddFilter }),
|
||||
findingsByResourceColumns.failed_findings,
|
||||
findingsByResourceColumns.compliance_score,
|
||||
],
|
||||
[onAddFilter]
|
||||
);
|
||||
|
@ -106,6 +102,7 @@ const baseColumns: Array<EuiTableFieldDataColumnType<FindingsByResourcePage>> =
|
|||
{
|
||||
...baseFindingsColumns['resource.id'],
|
||||
field: 'resource_id',
|
||||
width: '15%',
|
||||
render: (resourceId: FindingsByResourcePage['resource_id']) => (
|
||||
<Link
|
||||
to={generatePath(findingsNavigation.resource_findings.path, { resourceId })}
|
||||
|
@ -139,36 +136,21 @@ const baseColumns: Array<EuiTableFieldDataColumnType<FindingsByResourcePage>> =
|
|||
},
|
||||
baseFindingsColumns.cluster_id,
|
||||
{
|
||||
field: 'failed_findings',
|
||||
field: 'compliance_score',
|
||||
width: '150px',
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel"
|
||||
defaultMessage="Failed Findings"
|
||||
id="xpack.csp.findings.findingsByResourceTable.complianceScoreColumnLabel"
|
||||
defaultMessage="Compliance Score"
|
||||
/>
|
||||
),
|
||||
render: (failedFindings: FindingsByResourcePage['failed_findings']) => (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip',
|
||||
{
|
||||
defaultMessage: '{failed} out of {total}',
|
||||
values: {
|
||||
failed: failedFindings.count,
|
||||
total: failedFindings.total_findings,
|
||||
},
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<EuiTextColor color={failedFindings.count === 0 ? '' : 'danger'}>
|
||||
{formatNumber(failedFindings.count)}
|
||||
</EuiTextColor>
|
||||
<span> ({numeral(failedFindings.normalized).format('0%')})</span>
|
||||
</>
|
||||
</EuiToolTip>
|
||||
render: (complianceScore: FindingsByResourcePage['compliance_score'], data) => (
|
||||
<ComplianceScoreBar
|
||||
totalPassed={data.findings.passed_findings}
|
||||
totalFailed={data.findings.failed_findings}
|
||||
/>
|
||||
),
|
||||
dataType: 'number',
|
||||
},
|
||||
|
|
|
@ -57,8 +57,8 @@ const ResourceFindingsTableComponent = ({
|
|||
() => [
|
||||
getExpandColumn<CspFinding>({ onClick: setSelectedFinding }),
|
||||
createColumnWithFilters(baseFindingsColumns['result.evaluation'], { onAddFilter }),
|
||||
baseFindingsColumns['rule.benchmark.rule_number'],
|
||||
createColumnWithFilters(baseFindingsColumns['rule.name'], { onAddFilter }),
|
||||
createColumnWithFilters(baseFindingsColumns['rule.benchmark.name'], { onAddFilter }),
|
||||
baseFindingsColumns['rule.section'],
|
||||
baseFindingsColumns['@timestamp'],
|
||||
],
|
||||
|
|
|
@ -9,12 +9,12 @@ import { lastValueFrom } from 'rxjs';
|
|||
import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { Pagination } from '@elastic/eui';
|
||||
import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants';
|
||||
import { useKibana } from '../../../common/hooks/use_kibana';
|
||||
import { showErrorToast } from '../latest_findings/use_latest_findings';
|
||||
import type { FindingsBaseEsQuery, Sort } from '../types';
|
||||
import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils';
|
||||
import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants';
|
||||
import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants';
|
||||
|
||||
interface UseFindingsByResourceOptions extends FindingsBaseEsQuery {
|
||||
enabled: boolean;
|
||||
|
@ -35,11 +35,13 @@ type FindingsAggResponse = IKibanaSearchResponse<
|
|||
>;
|
||||
|
||||
export interface FindingsByResourcePage {
|
||||
failed_findings: {
|
||||
count: number;
|
||||
findings: {
|
||||
failed_findings: number;
|
||||
passed_findings: number;
|
||||
normalized: number;
|
||||
total_findings: number;
|
||||
};
|
||||
compliance_score: number;
|
||||
resource_id: string;
|
||||
cluster_id: string;
|
||||
'resource.name': string;
|
||||
|
@ -56,6 +58,8 @@ interface FindingsByResourceAggs {
|
|||
|
||||
interface FindingsAggBucket extends estypes.AggregationsStringRareTermsBucketKeys {
|
||||
failed_findings: estypes.AggregationsMultiBucketBase;
|
||||
compliance_score: estypes.AggregationsScriptedMetricAggregate;
|
||||
passed_findings: estypes.AggregationsMultiBucketBase;
|
||||
name: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringTermsBucketKeys>;
|
||||
subtype: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringTermsBucketKeys>;
|
||||
cluster_id: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringTermsBucketKeys>;
|
||||
|
@ -92,15 +96,27 @@ export const getFindingsByResourceAggQuery = ({
|
|||
failed_findings: {
|
||||
filter: { term: { 'result.evaluation': 'failed' } },
|
||||
},
|
||||
passed_findings: {
|
||||
filter: { term: { 'result.evaluation': 'passed' } },
|
||||
},
|
||||
cluster_id: {
|
||||
terms: { field: 'cluster_id', size: 1 },
|
||||
},
|
||||
sort_failed_findings: {
|
||||
compliance_score: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
passed: 'passed_findings>_count',
|
||||
failed: 'failed_findings>_count',
|
||||
},
|
||||
script: 'params.passed / (params.passed + params.failed)',
|
||||
},
|
||||
},
|
||||
sort_by_compliance_score: {
|
||||
bucket_sort: {
|
||||
size: MAX_FINDINGS_TO_LOAD,
|
||||
sort: [
|
||||
{
|
||||
'failed_findings>_count': { order: sortDirection },
|
||||
compliance_score: { order: sortDirection },
|
||||
_count: { order: 'desc' },
|
||||
_key: { order: 'asc' },
|
||||
},
|
||||
|
@ -177,11 +193,13 @@ const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResour
|
|||
cluster_id: resource.cluster_id.buckets[0]?.key,
|
||||
['rule.section']: resource.cis_sections.buckets.map((v) => v.key),
|
||||
['rule.benchmark.name']: resource.benchmarkName.buckets[0]?.key,
|
||||
failed_findings: {
|
||||
count: resource.failed_findings.doc_count,
|
||||
compliance_score: resource.compliance_score.value,
|
||||
findings: {
|
||||
failed_findings: resource.failed_findings.doc_count,
|
||||
normalized:
|
||||
resource.doc_count > 0 ? resource.failed_findings.doc_count / resource.doc_count : 0,
|
||||
total_findings: resource.doc_count,
|
||||
passed_findings: resource.passed_findings.doc_count,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -123,9 +123,10 @@ const baseColumns = [
|
|||
},
|
||||
{
|
||||
field: 'rule.name',
|
||||
name: i18n.translate('xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
name: i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel',
|
||||
{ defaultMessage: 'Rule Name' }
|
||||
),
|
||||
sortable: true,
|
||||
render: (name: string) => (
|
||||
<EuiToolTip content={name} position="left" anchorClassName="eui-textTruncate">
|
||||
|
@ -134,12 +135,29 @@ const baseColumns = [
|
|||
),
|
||||
},
|
||||
{
|
||||
field: 'rule.benchmark.name',
|
||||
field: 'rule.benchmark.rule_number',
|
||||
name: i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel',
|
||||
{ defaultMessage: 'Benchmark' }
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Rule Number',
|
||||
}
|
||||
),
|
||||
width: '120px',
|
||||
},
|
||||
{
|
||||
field: 'rule.benchmark.name',
|
||||
name: (
|
||||
<ColumnNameWithTooltip
|
||||
columnName={i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel',
|
||||
{ defaultMessage: 'Applicable Benchmark' }
|
||||
)}
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnTooltipLabel',
|
||||
{ defaultMessage: 'The benchmark(s) rules used to evaluate this resource came from' }
|
||||
)}
|
||||
/>
|
||||
),
|
||||
width: '10%',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
|
@ -149,7 +167,6 @@ const baseColumns = [
|
|||
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel',
|
||||
{ defaultMessage: 'CIS Section' }
|
||||
),
|
||||
width: '7%',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (section: string) => (
|
||||
|
@ -164,15 +181,14 @@ const baseColumns = [
|
|||
<ColumnNameWithTooltip
|
||||
columnName={i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnLabel',
|
||||
{ defaultMessage: 'Cluster ID' }
|
||||
{ defaultMessage: 'Belongs To' }
|
||||
)}
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnTooltipLabel',
|
||||
{ defaultMessage: 'Kube-System Namespace ID' }
|
||||
{ defaultMessage: 'Kubernetes Cluster ID or Cloud Account Name' }
|
||||
)}
|
||||
/>
|
||||
),
|
||||
width: '150px',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (section: string) => (
|
||||
|
@ -183,6 +199,7 @@ const baseColumns = [
|
|||
},
|
||||
{
|
||||
field: '@timestamp',
|
||||
align: 'right',
|
||||
width: '10%',
|
||||
name: i18n.translate(
|
||||
'xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { calculatePostureScore } from '../../../routes/compliance_dashboard/get_stats';
|
||||
import { calculatePostureScore } from '../../../../common/utils/helpers';
|
||||
import type { CspmAccountsStats } from './types';
|
||||
import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants';
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
QueryDslQueryContainer,
|
||||
SearchRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { calculatePostureScore } from './get_stats';
|
||||
import { calculatePostureScore } from '../../../common/utils/helpers';
|
||||
import type { ComplianceDashboardData } from '../../../common/types';
|
||||
import { KeyDocCount } from './compliance_dashboard';
|
||||
|
||||
|
|
|
@ -5,12 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
calculatePostureScore,
|
||||
FindingsEvaluationsQueryResult,
|
||||
getStatsFromFindingsEvaluationsAggs,
|
||||
roundScore,
|
||||
} from './get_stats';
|
||||
import { FindingsEvaluationsQueryResult, getStatsFromFindingsEvaluationsAggs } from './get_stats';
|
||||
import { calculatePostureScore, roundScore } from '../../../common/utils/helpers';
|
||||
|
||||
const standardQueryResult: FindingsEvaluationsQueryResult = {
|
||||
resources_evaluated: {
|
||||
|
|
|
@ -7,15 +7,8 @@
|
|||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ComplianceDashboardData, Score } from '../../../common/types';
|
||||
|
||||
/**
|
||||
* @param value value is [0, 1] range
|
||||
*/
|
||||
export const roundScore = (value: number): Score => Number((value * 100).toFixed(1));
|
||||
|
||||
export const calculatePostureScore = (passed: number, failed: number): Score =>
|
||||
roundScore(passed / (passed + failed));
|
||||
import { calculatePostureScore } from '../../../common/utils/helpers';
|
||||
import type { ComplianceDashboardData } from '../../../common/types';
|
||||
|
||||
export interface FindingsEvaluationsQueryResult {
|
||||
failed_findings: {
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClient } 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 { calculatePostureScore } from './get_stats';
|
||||
|
||||
export interface ScoreTrendDoc {
|
||||
'@timestamp': string;
|
||||
|
|
|
@ -28,7 +28,7 @@ function migrateCspRuleMetadata(
|
|||
policy_id,
|
||||
metadata: {
|
||||
...metadata,
|
||||
benchmark: { ...benchmark, id: 'cis_k8s' },
|
||||
benchmark: { ...benchmark, id: 'cis_k8s', rule_number: '' },
|
||||
impact: metadata.impact || undefined,
|
||||
default_value: metadata.default_value || undefined,
|
||||
references: metadata.references || undefined,
|
||||
|
|
|
@ -28,7 +28,7 @@ function migrateCspRuleMetadata(
|
|||
muted,
|
||||
metadata: {
|
||||
...metadata,
|
||||
benchmark: { ...benchmark, id: 'cis_k8s' },
|
||||
benchmark: { ...benchmark, id: 'cis_k8s', rule_number: '' },
|
||||
impact: metadata.impact || undefined,
|
||||
default_value: metadata.default_value || undefined,
|
||||
references: metadata.references || undefined,
|
||||
|
|
|
@ -10135,12 +10135,10 @@
|
|||
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " pour \"{name}\"",
|
||||
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} other {# intégrations}}",
|
||||
"xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode} : {body}",
|
||||
"xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}",
|
||||
"xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}",
|
||||
"xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}",
|
||||
"xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "Dernière évaluation {dateFromNow}",
|
||||
"xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "Affichage de {pageStart}-{pageEnd} sur {total} {type}",
|
||||
"xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip": "{failed} sur {total}",
|
||||
"xpack.csp.findings.findingsTableCell.addFilterButton": "Ajouter un filtre {field}",
|
||||
"xpack.csp.findings.findingsTableCell.addNegateFilterButton": "Ajouter un filtre {field} négatif",
|
||||
"xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - Résultats",
|
||||
|
@ -10198,7 +10196,6 @@
|
|||
"xpack.csp.findings.findingsByResource.noFindingsTitle": "Il n'y a aucun résultat",
|
||||
"xpack.csp.findings.findingsByResource.tableRowTypeLabel": "Ressources",
|
||||
"xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "Sections CIS",
|
||||
"xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel": "Échec des résultats",
|
||||
"xpack.csp.findings.findingsErrorToast.searchFailedTitle": "Échec de la recherche",
|
||||
"xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTab.actualTitle": "Réel",
|
||||
|
@ -10240,7 +10237,6 @@
|
|||
"xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "Type de ressource",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "Résultat",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "Benchmark",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel": "Règle",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "Section CIS",
|
||||
"xpack.csp.findings.groupBySelector.groupByLabel": "Regrouper par",
|
||||
"xpack.csp.findings.groupBySelector.groupByNoneLabel": "Aucun",
|
||||
|
|
|
@ -10124,12 +10124,10 @@
|
|||
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " \"{name}\"",
|
||||
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}を表示しています",
|
||||
"xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode}: {body}",
|
||||
"xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}",
|
||||
"xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}",
|
||||
"xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}",
|
||||
"xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "前回の評価{dateFromNow}",
|
||||
"xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "{total}件中{pageStart}-{pageEnd}件の{type}を表示しています",
|
||||
"xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip": "{total}件中{failed}件",
|
||||
"xpack.csp.findings.findingsTableCell.addFilterButton": "{field}フィルターを追加",
|
||||
"xpack.csp.findings.findingsTableCell.addNegateFilterButton": "{field}否定フィルターを追加",
|
||||
"xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - 調査結果",
|
||||
|
@ -10187,7 +10185,6 @@
|
|||
"xpack.csp.findings.findingsByResource.noFindingsTitle": "調査結果はありません",
|
||||
"xpack.csp.findings.findingsByResource.tableRowTypeLabel": "リソース",
|
||||
"xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "CISセクション",
|
||||
"xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel": "失敗した調査結果",
|
||||
"xpack.csp.findings.findingsErrorToast.searchFailedTitle": "検索失敗",
|
||||
"xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTab.actualTitle": "実際",
|
||||
|
@ -10229,7 +10226,6 @@
|
|||
"xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "リソースタイプ",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "結果",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "ベンチマーク",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel": "ルール",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "CISセクション",
|
||||
"xpack.csp.findings.groupBySelector.groupByLabel": "グループ分けの条件",
|
||||
"xpack.csp.findings.groupBySelector.groupByNoneLabel": "なし",
|
||||
|
|
|
@ -10139,12 +10139,10 @@
|
|||
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " 对于“{name}”",
|
||||
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "正在显示 {pageCount}/{totalCount, plural, other {# 个集成}}",
|
||||
"xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode}:{body}",
|
||||
"xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}",
|
||||
"xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}",
|
||||
"xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}",
|
||||
"xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "上次评估于 {dateFromNow}",
|
||||
"xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "正在显示第 {pageStart}-{pageEnd} 个(共 {total} 个){type}",
|
||||
"xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip": "{failed} 个(共 {total} 个)",
|
||||
"xpack.csp.findings.findingsTableCell.addFilterButton": "添加 {field} 筛选",
|
||||
"xpack.csp.findings.findingsTableCell.addNegateFilterButton": "添加 {field} 作废筛选",
|
||||
"xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - 结果",
|
||||
|
@ -10202,7 +10200,6 @@
|
|||
"xpack.csp.findings.findingsByResource.noFindingsTitle": "无结果",
|
||||
"xpack.csp.findings.findingsByResource.tableRowTypeLabel": "资源",
|
||||
"xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "CIS 部分",
|
||||
"xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel": "失败的结果",
|
||||
"xpack.csp.findings.findingsErrorToast.searchFailedTitle": "搜索失败",
|
||||
"xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTab.actualTitle": "实际",
|
||||
|
@ -10244,7 +10241,6 @@
|
|||
"xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "资源类型",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "结果",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "基准",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel": "规则",
|
||||
"xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "CIS 部分",
|
||||
"xpack.csp.findings.groupBySelector.groupByLabel": "分组依据",
|
||||
"xpack.csp.findings.groupBySelector.groupByNoneLabel": "无",
|
||||
|
|
|
@ -88,7 +88,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await filterBar.addFilter({ field: 'rule.name', operation: 'is', value: ruleName1 });
|
||||
|
||||
expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(true);
|
||||
});
|
||||
|
||||
it('remove filter', async () => {
|
||||
|
@ -102,8 +102,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await queryBar.setQuery(ruleName1);
|
||||
await queryBar.submitQuery();
|
||||
|
||||
expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule', ruleName2)).to.be(false);
|
||||
expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule Name', ruleName2)).to.be(false);
|
||||
|
||||
await queryBar.setQuery('');
|
||||
await queryBar.submitQuery();
|
||||
|
@ -114,18 +114,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
describe('Table Filters', () => {
|
||||
it('add cell value filter', async () => {
|
||||
await table.addCellFilter('Rule', ruleName1, false);
|
||||
await table.addCellFilter('Rule Name', ruleName1, false);
|
||||
|
||||
expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(true);
|
||||
});
|
||||
|
||||
it('add negated cell value filter', async () => {
|
||||
await table.addCellFilter('Rule', ruleName1, true);
|
||||
await table.addCellFilter('Rule Name', ruleName1, true);
|
||||
|
||||
expect(await filterBar.hasFilter('rule.name', ruleName1, true, false, true)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule', ruleName1)).to.be(false);
|
||||
expect(await table.hasColumnValue('Rule', ruleName2)).to.be(true);
|
||||
expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(false);
|
||||
expect(await table.hasColumnValue('Rule Name', ruleName2)).to.be(true);
|
||||
|
||||
await filterBar.removeFilter('rule.name');
|
||||
});
|
||||
|
@ -147,8 +147,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const testCases: TestCase[] = [
|
||||
['CIS Section', 'asc', sortByAlphabeticalOrder],
|
||||
['CIS Section', 'desc', sortByAlphabeticalOrder],
|
||||
['Cluster ID', 'asc', compareStringByLexicographicOrder],
|
||||
['Cluster ID', 'desc', compareStringByLexicographicOrder],
|
||||
['Resource ID', 'asc', compareStringByLexicographicOrder],
|
||||
['Resource ID', 'desc', compareStringByLexicographicOrder],
|
||||
['Resource Name', 'asc', sortByAlphabeticalOrder],
|
||||
['Resource Name', 'desc', sortByAlphabeticalOrder],
|
||||
['Resource Type', 'asc', sortByAlphabeticalOrder],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue