[Cloud Security] Removed errors thrown at resources table (#152944)

This commit is contained in:
Jordan 2023-03-16 23:35:39 -04:00 committed by GitHub
parent 72962abddf
commit 06c9924a15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 132 deletions

View file

@ -88,12 +88,12 @@ describe('<FindingsByResourceTable />', () => {
</TestProvider>
);
data.forEach((item, i) => {
data.forEach((item) => {
const row = screen.getByTestId(
TEST_SUBJECTS.getFindingsByResourceTableRowTestId(getResourceId(item))
);
expect(row).toBeInTheDocument();
expect(within(row).getByText(item.resource_id)).toBeInTheDocument();
expect(within(row).getByText(item.resource_id || '')).toBeInTheDocument();
if (item['resource.name'])
expect(within(row).getByText(item['resource.name'])).toBeInTheDocument();
if (item['resource.sub_type'])

View file

@ -44,7 +44,8 @@ interface Props {
}
export const getResourceId = (resource: FindingsByResourcePage) => {
return [resource.resource_id, ...resource['rule.section']].join('/');
const sections = resource['rule.section'] || [];
return [resource.resource_id, ...sections].join('/');
};
const FindingsByResourceTableComponent = ({
@ -81,9 +82,7 @@ const FindingsByResourceTableComponent = ({
getNonSortableColumn(findingsByResourceColumns['rule.benchmark.name']),
{ onAddFilter }
),
createColumnWithFilters(getNonSortableColumn(findingsByResourceColumns.belongs_to), {
onAddFilter,
}),
getNonSortableColumn(findingsByResourceColumns.belongs_to),
findingsByResourceColumns.compliance_score,
],
[onAddFilter]
@ -124,17 +123,21 @@ 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: encodeURIComponent(resourceId),
})}
className="eui-textTruncate"
title={resourceId}
>
{resourceId}
</Link>
),
render: (resourceId: FindingsByResourcePage['resource_id']) => {
if (!resourceId) return;
return (
<Link
to={generatePath(findingsNavigation.resource_findings.path, {
resourceId: encodeURIComponent(resourceId),
})}
className="eui-textTruncate"
title={resourceId}
>
{resourceId}
</Link>
);
},
},
baseFindingsColumns['resource.sub_type'],
baseFindingsColumns['resource.name'],

View file

@ -7,8 +7,17 @@
import { useQuery } from '@tanstack/react-query';
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 {
AggregationsCardinalityAggregate,
AggregationsMultiBucketAggregateBase,
AggregationsMultiBucketBase,
AggregationsScriptedMetricAggregate,
AggregationsStringRareTermsBucketKeys,
AggregationsStringTermsBucketKeys,
SearchRequest,
SearchResponse,
} from '@elastic/elasticsearch/lib/api/types';
import { getBelongsToRuntimeMapping } from '../../../../common/runtime_mappings/get_belongs_to_runtime_mapping';
import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants';
import { useKibana } from '../../../common/hooks/use_kibana';
@ -30,10 +39,8 @@ export interface FindingsByResourceQuery {
sortDirection: Sort<unknown>['direction'];
}
type FindingsAggRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type FindingsAggResponse = IKibanaSearchResponse<
estypes.SearchResponse<{}, FindingsByResourceAggs>
>;
type FindingsAggRequest = IKibanaSearchRequest<SearchRequest>;
type FindingsAggResponse = IKibanaSearchResponse<SearchResponse<{}, FindingsByResourceAggs>>;
export interface FindingsByResourcePage {
findings: {
@ -43,88 +50,86 @@ export interface FindingsByResourcePage {
total_findings: number;
};
compliance_score: number;
resource_id: string;
belongs_to: string;
'resource.name': string;
'resource.sub_type': string;
'rule.benchmark.name': string;
'rule.section': string[];
resource_id?: string;
belongs_to?: string;
'resource.name'?: string;
'resource.sub_type'?: string;
'rule.benchmark.name'?: string;
'rule.section'?: string[];
}
interface FindingsByResourceAggs {
resource_total: estypes.AggregationsCardinalityAggregate;
resources: estypes.AggregationsMultiBucketAggregateBase<FindingsAggBucket>;
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
resource_total: AggregationsCardinalityAggregate;
resources: AggregationsMultiBucketAggregateBase<FindingsAggBucket>;
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
}
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>;
belongs_to: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringTermsBucketKeys>;
benchmarkName: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
cis_sections: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
interface FindingsAggBucket extends AggregationsStringRareTermsBucketKeys {
failed_findings: AggregationsMultiBucketBase;
compliance_score: AggregationsScriptedMetricAggregate;
passed_findings: AggregationsMultiBucketBase;
name: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>;
subtype: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>;
belongs_to: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>;
benchmarkName: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
cis_sections: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
}
export const getFindingsByResourceAggQuery = ({
query,
sortDirection,
}: UseFindingsByResourceOptions): estypes.SearchRequest => ({
}: UseFindingsByResourceOptions): SearchRequest => ({
index: CSP_LATEST_FINDINGS_DATA_VIEW,
body: {
query,
size: 0,
runtime_mappings: getBelongsToRuntimeMapping(),
aggs: {
...getFindingsCountAggQuery(),
resource_total: { cardinality: { field: 'resource.id' } },
resources: {
terms: { field: 'resource.id', size: MAX_BUCKETS },
aggs: {
name: {
terms: { field: 'resource.name', size: 1 },
query,
size: 0,
runtime_mappings: getBelongsToRuntimeMapping(),
aggs: {
...getFindingsCountAggQuery(),
resource_total: { cardinality: { field: 'resource.id' } },
resources: {
terms: { field: 'resource.id', size: MAX_BUCKETS },
aggs: {
name: {
terms: { field: 'resource.name', size: 1 },
},
subtype: {
terms: { field: 'resource.sub_type', size: 1 },
},
benchmarkName: {
terms: { field: 'rule.benchmark.name' },
},
cis_sections: {
terms: { field: 'rule.section' },
},
failed_findings: {
filter: { term: { 'result.evaluation': 'failed' } },
},
passed_findings: {
filter: { term: { 'result.evaluation': 'passed' } },
},
// this field is runtime generated
belongs_to: {
terms: { field: 'belongs_to', size: 1 },
},
compliance_score: {
bucket_script: {
buckets_path: {
passed: 'passed_findings>_count',
failed: 'failed_findings>_count',
},
script: 'params.passed / (params.passed + params.failed)',
},
subtype: {
terms: { field: 'resource.sub_type', size: 1 },
},
benchmarkName: {
terms: { field: 'rule.benchmark.name' },
},
cis_sections: {
terms: { field: 'rule.section' },
},
failed_findings: {
filter: { term: { 'result.evaluation': 'failed' } },
},
passed_findings: {
filter: { term: { 'result.evaluation': 'passed' } },
},
// this field is runtime generated
belongs_to: {
terms: { field: 'belongs_to', size: 1 },
},
compliance_score: {
bucket_script: {
buckets_path: {
passed: 'passed_findings>_count',
failed: 'failed_findings>_count',
},
sort_by_compliance_score: {
bucket_sort: {
size: MAX_FINDINGS_TO_LOAD,
sort: [
{
compliance_score: { order: sortDirection },
_count: { order: 'desc' },
_key: { order: 'asc' },
},
script: 'params.passed / (params.passed + params.failed)',
},
},
sort_by_compliance_score: {
bucket_sort: {
size: MAX_FINDINGS_TO_LOAD,
sort: [
{
compliance_score: { order: sortDirection },
_count: { order: 'desc' },
_key: { order: 'asc' },
},
],
},
],
},
},
},
@ -133,6 +138,35 @@ export const getFindingsByResourceAggQuery = ({
ignore_unavailable: false,
});
const getFirstKey = (
buckets: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>['buckets']
): undefined | string => {
if (!!Array.isArray(buckets) && !!buckets.length) return buckets[0].key;
};
const getKeysList = (
buckets: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>['buckets']
): undefined | string[] => {
if (!!Array.isArray(buckets) && !!buckets.length) return buckets.map((v) => v.key);
};
const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResourcePage => ({
resource_id: resource.key,
['resource.name']: getFirstKey(resource.name.buckets),
['resource.sub_type']: getFirstKey(resource.subtype.buckets),
['rule.section']: getKeysList(resource.cis_sections.buckets),
['rule.benchmark.name']: getFirstKey(resource.benchmarkName.buckets),
belongs_to: getFirstKey(resource.belongs_to.buckets),
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,
},
});
export const useFindingsByResource = (options: UseFindingsByResourceOptions) => {
const {
data,
@ -152,16 +186,18 @@ export const useFindingsByResource = (options: UseFindingsByResourceOptions) =>
})
);
if (!aggregations) throw new Error('expected aggregations to be defined');
if (!aggregations) throw new Error('Failed to aggregate by, missing resource id');
if (
!Array.isArray(aggregations.resources.buckets) ||
!Array.isArray(aggregations.count.buckets)
)
throw new Error('expected buckets to be an array');
throw new Error('Failed to group by, missing resource id');
const page = aggregations.resources.buckets.map(createFindingsByResource);
return {
page: aggregations.resources.buckets.map(createFindingsByResource),
page,
total: aggregations.resource_total.value,
count: getAggregationCount(aggregations.count.buckets),
};
@ -173,36 +209,3 @@ export const useFindingsByResource = (options: UseFindingsByResourceOptions) =>
}
);
};
const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResourcePage => {
if (
!Array.isArray(resource.benchmarkName.buckets) ||
!Array.isArray(resource.cis_sections.buckets) ||
!Array.isArray(resource.name.buckets) ||
!Array.isArray(resource.subtype.buckets) ||
!Array.isArray(resource.belongs_to.buckets) ||
!resource.benchmarkName.buckets.length ||
!resource.cis_sections.buckets.length ||
!resource.name.buckets.length ||
!resource.subtype.buckets.length ||
!resource.belongs_to.buckets.length
)
throw new Error('expected buckets to be an array');
return {
resource_id: resource.key,
['resource.name']: resource.name.buckets[0]?.key,
['resource.sub_type']: resource.subtype.buckets[0]?.key,
['rule.section']: resource.cis_sections.buckets.map((v) => v.key),
['rule.benchmark.name']: resource.benchmarkName.buckets[0]?.key,
belongs_to: resource.belongs_to.buckets[0]?.key,
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,
},
};
};

View file

@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
import type { Serializable } from '@kbn/utility-types';
import { FormattedMessage } from '@kbn/i18n-react';
import { FindingsByResourcePage } from '../latest_findings_by_resource/use_findings_by_resource';
import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants';
import { TimestampTableCell } from '../../../components/timestamp_table_cell';
import { ColumnNameWithTooltip } from '../../../components/column_name_with_tooltip';
@ -117,11 +118,15 @@ const baseColumns = [
),
sortable: true,
truncateText: true,
render: (name: string) => (
<EuiToolTip content={name} position="left" anchorClassName="eui-textTruncate">
<>{name}</>
</EuiToolTip>
),
render: (name: FindingsByResourcePage['resource.name']) => {
if (!name) return;
return (
<EuiToolTip content={name} position="left" anchorClassName="eui-textTruncate">
<>{name}</>
</EuiToolTip>
);
},
},
{
field: 'rule.name',