mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Security] Removed errors thrown at resources table (#152944)
This commit is contained in:
parent
72962abddf
commit
06c9924a15
4 changed files with 143 additions and 132 deletions
|
@ -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'])
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue