diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx index a6b8f3b8634018..9cc87d98e54f84 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx @@ -21,14 +21,23 @@ import { TestProvider } from '../../../test/test_provider'; const chance = new Chance(); -const getFakeFindingsByResource = (): CspFindingsByResource => ({ - resource_id: chance.guid(), - cis_sections: [chance.word(), chance.word()], - failed_findings: { - total: chance.integer(), - normalized: chance.integer({ min: 0, max: 1 }), - }, -}); +const getFakeFindingsByResource = (): CspFindingsByResource => { + const count = chance.integer(); + const total = chance.integer() + count + 1; + const normalized = count / total; + + return { + resource_id: chance.guid(), + resource_name: chance.word(), + resource_subtype: chance.word(), + cis_sections: [chance.word(), chance.word()], + failed_findings: { + count, + normalized, + total_findings: total, + }, + }; +}; type TableProps = PropsOf; @@ -74,8 +83,11 @@ describe('', () => { ); expect(row).toBeInTheDocument(); expect(within(row).getByText(item.resource_id)).toBeInTheDocument(); + if (item.resource_name) expect(within(row).getByText(item.resource_name)).toBeInTheDocument(); + if (item.resource_subtype) + expect(within(row).getByText(item.resource_subtype)).toBeInTheDocument(); expect(within(row).getByText(item.cis_sections.join(', '))).toBeInTheDocument(); - expect(within(row).getByText(formatNumber(item.failed_findings.total))).toBeInTheDocument(); + expect(within(row).getByText(formatNumber(item.failed_findings.count))).toBeInTheDocument(); expect( within(row).getByText(new RegExp(numeral(item.failed_findings.normalized).format('0%'))) ).toBeInTheDocument(); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx index 2e96306ad3a694..80da9222258934 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx @@ -9,12 +9,12 @@ import { EuiEmptyPrompt, EuiBasicTable, EuiTextColor, - EuiFlexGroup, - EuiFlexItem, type EuiTableFieldDataColumnType, type CriteriaWithPagination, type Pagination, + EuiToolTip, } 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'; @@ -81,6 +81,26 @@ const columns: Array> = [ ), }, + { + field: 'resource_subtype', + truncateText: true, + name: ( + + ), + }, + { + field: 'resource_name', + truncateText: true, + name: ( + + ), + }, { field: 'cis_sections', truncateText: true, @@ -102,14 +122,22 @@ const columns: Array> = [ /> ), render: (failedFindings: CspFindingsByResource['failed_findings']) => ( - - - {formatNumber(failedFindings.total)} - - - ({numeral(failedFindings.normalized).format('0%')}) - - + + <> + + {formatNumber(failedFindings.count)} + + ({numeral(failedFindings.normalized).format('0%')}) + + ), }, ]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts index 880b2be868e6f4..e2da77c8ba2a21 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts @@ -14,7 +14,7 @@ import { showErrorToast } from '../latest_findings/use_latest_findings'; import type { FindingsBaseEsQuery, FindingsQueryResult } from '../types'; // a large number to probably get all the buckets -const MAX_BUCKETS = 60 * 1000; +const MAX_BUCKETS = 1000 * 1000; interface UseResourceFindingsOptions extends FindingsBaseEsQuery { from: NonNullable; @@ -43,6 +43,8 @@ interface FindingsByResourceAggs { interface FindingsAggBucket extends estypes.AggregationsStringRareTermsBucketKeys { failed_findings: estypes.AggregationsMultiBucketBase; + name: estypes.AggregationsMultiBucketAggregateBase; + subtype: estypes.AggregationsMultiBucketAggregateBase; cis_sections: estypes.AggregationsMultiBucketAggregateBase; } @@ -57,10 +59,16 @@ export const getFindingsByResourceAggQuery = ({ query, size: 0, aggs: { - resource_total: { cardinality: { field: 'resource.id.keyword' } }, + resource_total: { cardinality: { field: 'resource.id' } }, resources: { - terms: { field: 'resource.id.keyword', size: MAX_BUCKETS }, + terms: { field: 'resource.id', size: MAX_BUCKETS }, aggs: { + name: { + terms: { field: 'resource.name', size: 1 }, + }, + subtype: { + terms: { field: 'resource.sub_type', size: 1 }, + }, cis_sections: { terms: { field: 'rule.section.keyword' }, }, @@ -117,16 +125,24 @@ export const useFindingsByResource = ({ index, query, from, size }: UseResourceF ); }; -const createFindingsByResource = (bucket: FindingsAggBucket) => { - if (!Array.isArray(bucket.cis_sections.buckets)) +const createFindingsByResource = (resource: FindingsAggBucket) => { + if ( + !Array.isArray(resource.cis_sections.buckets) || + !Array.isArray(resource.name.buckets) || + !Array.isArray(resource.subtype.buckets) + ) throw new Error('expected buckets to be an array'); return { - resource_id: bucket.key, - cis_sections: bucket.cis_sections.buckets.map((v) => v.key), + resource_id: resource.key, + resource_name: resource.name.buckets.map((v) => v.key).at(0), + resource_subtype: resource.subtype.buckets.map((v) => v.key).at(0), + cis_sections: resource.cis_sections.buckets.map((v) => v.key), failed_findings: { - total: bucket.failed_findings.doc_count, - normalized: bucket.doc_count > 0 ? bucket.failed_findings.doc_count / bucket.doc_count : 0, + count: resource.failed_findings.doc_count, + normalized: + resource.doc_count > 0 ? resource.failed_findings.doc_count / resource.doc_count : 0, + total_findings: resource.doc_count, }, }; }; diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts index 9ebe4c3cf4038e..57305fd2df7c42 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts @@ -50,20 +50,32 @@ export const latestFindingsMapping: MappingTypeMapping = { properties: { type: { type: 'keyword', - ignore_above: 256, + ignore_above: 1024, }, id: { - type: 'text', + type: 'keyword', + ignore_above: 1024, + fields: { + text: { + type: 'text', + }, + }, }, name: { - type: 'text', + type: 'keyword', + ignore_above: 1024, + fields: { + text: { + type: 'text', + }, + }, }, sub_type: { - type: 'text', + ignore_above: 1024, + type: 'keyword', fields: { - keyword: { - ignore_above: 1024, - type: 'keyword', + text: { + type: 'text', }, }, },