From 8bbb01a4475703ab973ec719222b0347aa63632f Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 13 Oct 2023 16:29:02 -0700 Subject: [PATCH 01/63] wip: grouping --- .../latest_findings_container.tsx | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 9abae7af4a211..6f36676de8c6d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -9,6 +9,7 @@ import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elast import { i18n } from '@kbn/i18n'; import { DataTableRecord } from '@kbn/discover-utils/types'; import { Filter, Query } from '@kbn/es-query'; +import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolution-grouping'; import { TimestampTableCell } from '../../../components/timestamp_table_cell'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import type { Evaluation } from '../../../../common/types'; @@ -27,6 +28,7 @@ import { CloudSecurityDefaultColumn, } from '../../../components/cloud_security_data_table'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; +import { useFindingsByResource } from '../latest_findings_by_resource/use_findings_by_resource'; const getDefaultQuery = ({ query, @@ -107,6 +109,12 @@ const customCellRenderer = (rows: DataTableRecord[]) => ({ ), }); +export const FINDINGS_UNIT = (totalCount: number) => + i18n.translate('xpack.csp.findings.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`, + }); + export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { const cloudPostureTable = useCloudPostureTable({ dataView, @@ -115,6 +123,188 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { defaultQuery: getDefaultQuery, }); + const { query, sort } = cloudPostureTable; + + const grouping = useGrouping({ + componentProps: { + // groupPanelRenderer: renderGroupPanel, + // groupStatsRenderer: getStats, + // onGroupToggle, + unit: FINDINGS_UNIT, + }, + defaultGroupingOptions: [ + { + label: 'Rule Name', + key: 'kibana.alert.rule.name', + }, + ], + fields: dataView.fields, + groupingId: 'latestFindings', + maxGroupingLevels: 2, + onGroupChange: (params) => { + console.log('onGroupChange', params); + }, + onOptionsChange: (options) => { + console.log('onOptionsChange', options); + }, + }); + + const groupingQuery = getGroupingQuery({ + additionalFilters: [], + from: 'now-1y', + groupByField: grouping.selectedGroups.join(','), + uniqueValue: grouping.selectedGroups.join(','), + to: 'now', + // groupByField, + // pageNumber, + // rootAggregations, + // runtimeMappings, + // size = DEFAULT_GROUP_BY_FIELD_SIZE, + // sort, + // statsAggregations, + // to, + // uniqueValue, + }); + + console.log({ groupingQuery }); + console.log({ grouping }); + console.log(grouping.selectedGroups); + + const { data } = useFindingsByResource({ + sortDirection: cloudPostureTable.urlQuery.sort.direction, + query, + enabled: true, + }); + + console.log({ data }); + + const GroupSelector = () => grouping.groupSelector; + + return ( + <> + {/* */} + {grouping.getGrouping({ + activePage: 0, + data: { + groupsCount: { + value: 1, + }, + groupByFields: { + buckets: [ + { + key: ['Vulnerability: CVE-2022-28734'], + doc_count: 41, + hostsCountAggregation: { + value: 25, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'CNVM', + doc_count: 41, + }, + { + key: 'CVE-2022-28734', + doc_count: 41, + }, + { + key: 'Cloud Security', + doc_count: 41, + }, + { + key: 'Data Source: Cloud Native Vulnerability Management', + doc_count: 41, + }, + { + key: 'OS: Linux', + doc_count: 41, + }, + { + key: 'Use Case: Vulnerability', + doc_count: 41, + }, + ], + }, + unitsCount: { + value: 41, + }, + description: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: "Out-of-bounds write when handling split HTTP headers; When handling split HTTP headers, GRUB2 HTTP code accidentally moves its internal data buffer point by one position. This can lead to a out-of-bound write further when parsing the HTTP request, writing a NULL byte past the buffer. It's conceivable that an attacker controlled set of packets can lead to corruption of the GRUB2's internal memory metadata.", + doc_count: 41, + }, + ], + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + { + key: 'medium', + doc_count: 13, + }, + { + key: 'high', + doc_count: 10, + }, + ], + }, + countSeveritySubAggregation: { + value: 3, + }, + usersCountAggregation: { + value: 0, + }, + selectedGroup: 'kibana.alert.rule.name', + key_as_string: 'Vulnerability: CVE-2022-28734', + }, + ], + }, + unitsCount: { + value: 41, + }, + }, + groupingLevel: 0, + inspectButton: undefined, + isLoading: false, + itemsPerPage: 10, + onChangeGroupsItemsPerPage: () => { + console.log('onChangeGroupsItemsPerPage'); + }, + onChangeGroupsPage: () => { + console.log('onChangeGroupsPage'); + }, + renderChildComponent: (groupFilter) => { + return
{JSON.stringify(groupFilter)}
; + }, + onGroupClose: () => { + console.log('onGroupClose'); + }, + selectedGroup: 'kibana.alert.rule.name', + // selectedGroup: 'kibana.alert.rule.name', + takeActionItems: () => [], + })} + + ); +}; + +export const LatestFindingsContainerOld = ({ dataView }: FindingsBaseProps) => { + const cloudPostureTable = useCloudPostureTable({ + dataView, + paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, + columnsLocalStorageKey, + defaultQuery: getDefaultQuery, + }); + const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureTable; const { From c40e2fc59b09ad35c16757fd22c32b7c434d2032 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 17 Oct 2023 11:38:55 -0700 Subject: [PATCH 02/63] group selector fix --- .../src/components/group_selector/index.tsx | 7 +++++-- .../src/hooks/use_get_group_selector.tsx | 14 +++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx index 3fa82f7f8af20..84532a3802985 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx @@ -57,7 +57,10 @@ const GroupSelectorComponent = ({ }, ...options.map((o) => ({ 'data-test-subj': `panel-${o.key}`, - disabled: groupsSelected.length === maxGroupingLevels && !isGroupSelected(o.key), + disabled: + maxGroupingLevels > 1 && + groupsSelected.length === maxGroupingLevels && + !isGroupSelected(o.key), name: o.label, onClick: () => onGroupChange(o.key), icon: isGroupSelected(o.key) ? 'check' : 'empty', @@ -66,7 +69,7 @@ const GroupSelectorComponent = ({ 'data-test-subj': `panel-custom`, name: i18n.CUSTOM_FIELD, icon: 'empty', - disabled: groupsSelected.length === maxGroupingLevels, + disabled: maxGroupingLevels > 1 && groupsSelected.length === maxGroupingLevels, panel: 'customPanel', hasPanel: true, }, diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index d50809c99fb3f..7aede5c249ad0 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -120,10 +120,14 @@ export const useGetGroupSelector = ({ return; } - const newSelectedGroups = isNoneGroup([groupSelection]) - ? [groupSelection] - : [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection]; - setSelectedGroups(newSelectedGroups); + if (maxGroupingLevels === 1) { + setSelectedGroups([groupSelection]); + } else { + const newSelectedGroups = isNoneGroup([groupSelection]) + ? [groupSelection] + : [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection]; + setSelectedGroups(newSelectedGroups); + } // built-in telemetry: UI-counter tracker?.( @@ -133,7 +137,7 @@ export const useGetGroupSelector = ({ onGroupChange?.({ tableId: groupingId, groupByField: groupSelection }); }, - [groupingId, onGroupChange, selectedGroups, setSelectedGroups, tracker] + [groupingId, maxGroupingLevels, onGroupChange, selectedGroups, setSelectedGroups, tracker] ); useEffect(() => { From 0ac7e9ed218b557941db700537d80054fe222eb0 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 17 Oct 2023 11:39:22 -0700 Subject: [PATCH 03/63] WIP: group selector on findings page --- .../additional_controls.tsx | 25 +- .../cloud_security_data_table.tsx | 2 + .../cloud_security_data_table/use_styles.ts | 3 - .../latest_findings_container.tsx | 243 +++++++++--------- .../latest_findings/use_grouped_findings.tsx | 126 +++++++++ 5 files changed, 266 insertions(+), 133 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx index 61a85e9993ecf..3ed680c4d8317 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx @@ -12,6 +12,7 @@ import numeral from '@elastic/numeral'; import { FieldsSelectorModal } from './fields_selector'; import { FindingsGroupBySelector } from '../../pages/configurations/layout/findings_group_by_selector'; import { useStyles } from './use_styles'; +import { AnyAsyncThunk } from '@reduxjs/toolkit/dist/matchers'; const formatNumber = (value: number) => { return value < 1000 ? value : numeral(value).format('0.0a'); @@ -24,6 +25,7 @@ export const AdditionalControls = ({ columns, onAddColumn, onRemoveColumn, + groupSelector, }: { total: number; title: string; @@ -31,6 +33,7 @@ export const AdditionalControls = ({ columns: string[]; onAddColumn: (column: string) => void; onRemoveColumn: (column: string) => void; + groupSelector: any; }) => { const styles = useStyles(); @@ -39,6 +42,24 @@ export const AdditionalControls = ({ const closeModal = () => setIsFieldSelectorModalVisible(false); const showModal = () => setIsFieldSelectorModalVisible(true); + const GroupSelector = () => { + if (groupSelector === null) { + return null; + } + if (groupSelector) { + return ( + + {groupSelector} + + ); + } + return ( + + ; + + ); + }; + return ( <> {isFieldSelectorModalVisible && ( @@ -66,9 +87,7 @@ export const AdditionalControls = ({ })} - - - + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx index 12afa013aed18..422dd7dc268ed 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx @@ -86,6 +86,7 @@ export const CloudSecurityDataTable = ({ loadMore, title, customCellRenderer, + groupSelector, ...rest }: CloudSecurityDataGridProps) => { const { @@ -213,6 +214,7 @@ export const CloudSecurityDataTable = ({ columns={currentColumns} onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} + groupSelector={groupSelector} /> ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/use_styles.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/use_styles.ts index b3b0fa1b172b1..3919e01af7753 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/use_styles.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/use_styles.ts @@ -23,9 +23,6 @@ export const useStyles = () => { border-bottom: none; margin-bottom: ${euiTheme.size.s}; border-top: none; - & .euiButtonEmpty { - font-weight: ${euiTheme.font.weight.bold}; - } } & .euiDataGrid--headerUnderline .euiDataGridHeaderCell { border-bottom: ${euiTheme.border.width.thick} solid ${euiTheme.colors.fullShade}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 6f36676de8c6d..420407927caf2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -4,12 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataTableRecord } from '@kbn/discover-utils/types'; import { Filter, Query } from '@kbn/es-query'; import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolution-grouping'; +import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; import { TimestampTableCell } from '../../../components/timestamp_table_cell'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import type { Evaluation } from '../../../../common/types'; @@ -28,7 +29,8 @@ import { CloudSecurityDefaultColumn, } from '../../../components/cloud_security_data_table'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; -import { useFindingsByResource } from '../latest_findings_by_resource/use_findings_by_resource'; +// import { useFindingsByResource } from '../latest_findings_by_resource/use_findings_by_resource'; +import { useGroupedFindings } from './use_grouped_findings'; const getDefaultQuery = ({ query, @@ -47,7 +49,7 @@ const getDefaultQuery = ({ const defaultColumns: CloudSecurityDefaultColumn[] = [ { id: 'result.evaluation', width: 80 }, { id: 'resource.id' }, - { id: 'resource.name' }, + { id: 'resource.id' }, { id: 'resource.sub_type' }, { id: 'rule.benchmark.rule_number' }, { id: 'rule.name' }, @@ -116,14 +118,14 @@ export const FINDINGS_UNIT = (totalCount: number) => }); export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { - const cloudPostureTable = useCloudPostureTable({ - dataView, - paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, - columnsLocalStorageKey, - defaultQuery: getDefaultQuery, - }); + // const cloudPostureTable = useCloudPostureTable({ + // dataView, + // paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, + // columnsLocalStorageKey, + // defaultQuery: getDefaultQuery, + // }); - const { query, sort } = cloudPostureTable; + // const { query, sort } = cloudPostureTable; const grouping = useGrouping({ componentProps: { @@ -134,28 +136,31 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { }, defaultGroupingOptions: [ { - label: 'Rule Name', - key: 'kibana.alert.rule.name', + label: 'Resource', + key: 'resource.id', }, ], fields: dataView.fields, - groupingId: 'latestFindings', - maxGroupingLevels: 2, - onGroupChange: (params) => { - console.log('onGroupChange', params); - }, - onOptionsChange: (options) => { - console.log('onOptionsChange', options); - }, + groupingId: 'cspLatestFindings', + maxGroupingLevels: 1, + // onGroupChange: (params) => { + // console.log('onGroupChange', params); + // }, + // onOptionsChange: (options) => { + // console.log('onOptionsChange', options); + // }, }); + const selectedGroup = grouping.selectedGroups[0]; + const isNoneSelected = isNoneGroup(grouping.selectedGroups); + console.log({ selectedGroup }); + const groupingQuery = getGroupingQuery({ additionalFilters: [], from: 'now-1y', - groupByField: grouping.selectedGroups.join(','), - uniqueValue: grouping.selectedGroups.join(','), + groupByField: selectedGroup, + uniqueValue: selectedGroup, to: 'now', - // groupByField, // pageNumber, // rootAggregations, // runtimeMappings, @@ -166,116 +171,38 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { // uniqueValue, }); - console.log({ groupingQuery }); - console.log({ grouping }); - console.log(grouping.selectedGroups); - - const { data } = useFindingsByResource({ - sortDirection: cloudPostureTable.urlQuery.sort.direction, - query, - enabled: true, + const { data, isFetching } = useGroupedFindings({ + // sortDirection: cloudPostureTable.urlQuery.sort.direction, + query: groupingQuery, + enabled: !isNoneSelected, }); - console.log({ data }); + const aggs = useMemo( + // queriedGroup because `selectedGroup` updates before the query response + () => + parseGroupingQuery( + // fallback to selectedGroup if queriedGroup.current is null, this happens in tests + selectedGroup, + selectedGroup, + data + ), + [data, selectedGroup] + ); - const GroupSelector = () => grouping.groupSelector; + if (isNoneSelected) { + return ( + + ); + } return ( <> - {/* */} {grouping.getGrouping({ activePage: 0, - data: { - groupsCount: { - value: 1, - }, - groupByFields: { - buckets: [ - { - key: ['Vulnerability: CVE-2022-28734'], - doc_count: 41, - hostsCountAggregation: { - value: 25, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'CNVM', - doc_count: 41, - }, - { - key: 'CVE-2022-28734', - doc_count: 41, - }, - { - key: 'Cloud Security', - doc_count: 41, - }, - { - key: 'Data Source: Cloud Native Vulnerability Management', - doc_count: 41, - }, - { - key: 'OS: Linux', - doc_count: 41, - }, - { - key: 'Use Case: Vulnerability', - doc_count: 41, - }, - ], - }, - unitsCount: { - value: 41, - }, - description: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: "Out-of-bounds write when handling split HTTP headers; When handling split HTTP headers, GRUB2 HTTP code accidentally moves its internal data buffer point by one position. This can lead to a out-of-bound write further when parsing the HTTP request, writing a NULL byte past the buffer. It's conceivable that an attacker controlled set of packets can lead to corruption of the GRUB2's internal memory metadata.", - doc_count: 41, - }, - ], - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'critical', - doc_count: 18, - }, - { - key: 'medium', - doc_count: 13, - }, - { - key: 'high', - doc_count: 10, - }, - ], - }, - countSeveritySubAggregation: { - value: 3, - }, - usersCountAggregation: { - value: 0, - }, - selectedGroup: 'kibana.alert.rule.name', - key_as_string: 'Vulnerability: CVE-2022-28734', - }, - ], - }, - unitsCount: { - value: 41, - }, - }, + data: aggs, groupingLevel: 0, inspectButton: undefined, - isLoading: false, + isLoading: isFetching, itemsPerPage: 10, onChangeGroupsItemsPerPage: () => { console.log('onChangeGroupsItemsPerPage'); @@ -284,20 +211,81 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { console.log('onChangeGroupsPage'); }, renderChildComponent: (groupFilter) => { - return
{JSON.stringify(groupFilter)}
; + console.log({ groupFilter }); + return ; }, onGroupClose: () => { console.log('onGroupClose'); }, - selectedGroup: 'kibana.alert.rule.name', - // selectedGroup: 'kibana.alert.rule.name', + selectedGroup, + // selectedGroup: 'resource.id', takeActionItems: () => [], })} ); }; -export const LatestFindingsContainerOld = ({ dataView }: FindingsBaseProps) => { +export const LatestFindingsContainerTable = ({ dataView, filter }: FindingsBaseProps) => { + const cloudPostureTable = useCloudPostureTable({ + dataView, + paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, + columnsLocalStorageKey, + defaultQuery: getDefaultQuery, + }); + + const { query, sort, queryError, setUrlQuery, getRowsFromPages } = cloudPostureTable; + + useEffect(() => { + setUrlQuery({ + filters: filter, + }); + }, [filter, setUrlQuery]); + + const { + data, + error: fetchError, + isFetching, + fetchNextPage, + } = useLatestFindings({ + query, + sort, + enabled: !queryError, + }); + + const rows = useMemo(() => getRowsFromPages(data?.pages), [data?.pages, getRowsFromPages]); + + const error = fetchError || queryError; + const total = data?.pages[0].total || 0; + + return ( + + + + {error && } + {!error && ( + <> + + + + )} + + ); +}; + +export const LatestFindingsContainerOld = ({ dataView, groupSelector }: FindingsBaseProps) => { const cloudPostureTable = useCloudPostureTable({ dataView, paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, @@ -365,6 +353,7 @@ export const LatestFindingsContainerOld = ({ dataView }: FindingsBaseProps) => { loadMore={fetchNextPage} title={title} customCellRenderer={customCellRenderer} + groupSelector={groupSelector} /> )} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx new file mode 100644 index 0000000000000..af5f6fe62b395 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx @@ -0,0 +1,126 @@ +/* + * 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 { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; +import { GroupingAggregation } from '@kbn/securitysolution-grouping'; +import { GenericBuckets } from '@kbn/securitysolution-grouping/src'; +import { useQuery } from '@tanstack/react-query'; +import { lastValueFrom } from 'rxjs'; +import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; +import { useKibana } from '../../../common/hooks/use_kibana'; +import { showErrorToast } from '../../../common/utils/show_error_toast'; + +// Elasticsearch returns `null` when a sub-aggregation cannot be computed +type NumberOrNull = number | null; + +export interface FindingsGroupingAggregation { + unitsCount?: { + value?: NumberOrNull; + }; + description?: { + buckets?: GenericBuckets[]; + }; + severitiesSubAggregation?: { + buckets?: GenericBuckets[]; + }; + countSeveritySubAggregation?: { + value?: NumberOrNull; + }; + usersCountAggregation?: { + value?: NumberOrNull; + }; + hostsCountAggregation?: { + value?: NumberOrNull; + }; + ipsCountAggregation?: { + value?: NumberOrNull; + }; + rulesCountAggregation?: { + value?: NumberOrNull; + }; + ruleTags?: { + doc_count_error_upper_bound?: number; + sum_other_doc_count?: number; + buckets?: GenericBuckets[]; + }; + stackByMultipleFields1?: { + buckets?: GenericBuckets[]; + doc_count_error_upper_bound?: number; + sum_other_doc_count?: number; + }; +} + +export const getGroupedFindingsQuery = (query: any) => ({ + index: CSP_LATEST_FINDINGS_DATA_VIEW, + ...query, +}); + +export const useGroupedFindings = ({ query, enabled = true }: any) => { + const { + data, + notifications: { toasts }, + } = useKibana().services; + + return useQuery( + ['csp_grouped_findings', { query }], + async () => { + const { + rawResponse: { aggregations }, + } = await lastValueFrom( + data.search.search< + {}, + IKibanaSearchResponse< + SearchResponse<{}, GroupingAggregation> + > + >({ + params: getGroupedFindingsQuery(query), + }) + ); + + 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('Failed to group by, missing resource id'); + + // const page = aggregations.resources.buckets.map(createFindingsByResource); + + // const aggs = parseGroupingQuery( + // // fallback to selectedGroup if queriedGroup.current is null, this happens in tests + // queriedGroup.current === null ? selectedGroup : queriedGroup.current, + // uniqueValue, + // alertsGroupsData?.aggregations + // ); + + return aggregations; + // return { + // ...aggregations, + // groupByFields: { + // ...aggregations.groupByFields, + // buckets: [ + // ...aggregations.groupByFields.buckets.map((bucket) => ({ + // ...bucket, + // unitsCount: { + // value: 2, + // }, + // selectedGroup: 'resource.id', + // key_as_string: 'Vulnerability: CVE-2022-28734', + // key: ['Vulnerability: CVE-2022-28734'], + // })), + // ], + // }, + // }; + }, + { + onError: (err: Error) => showErrorToast(toasts, err), + enabled, + } + ); +}; From bb7f3082a88cde94bd511328f944ff9b8d1bcd17 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 18 Oct 2023 12:11:55 -0700 Subject: [PATCH 04/63] adding more group by fields --- .../latest_findings/latest_findings_container.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 420407927caf2..d0b8c5e90e3f6 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -139,6 +139,18 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { label: 'Resource', key: 'resource.id', }, + { + label: 'Rule name', + key: 'rule.name', + }, + { + label: 'Cloud account', + key: 'cloud.account.name', + }, + { + label: 'Kubernetes cluster', + key: 'orchestrator.cluster.name', + }, ], fields: dataView.fields, groupingId: 'cspLatestFindings', From c9f251ee99bad7280cc5c04e679ec13167fcee9a Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 23 Oct 2023 15:51:07 -0700 Subject: [PATCH 05/63] add non url additional filters --- .../use_cloud_posture_table.ts | 11 ++- .../hooks/use_cloud_posture_table/utils.ts | 21 +++-- .../public/common/types.ts | 5 ++ .../latest_findings_container.tsx | 76 ++++++++++--------- .../latest_findings/use_grouped_findings.tsx | 33 +------- 5 files changed, 67 insertions(+), 79 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts index 0becb56e6ec22..c6de939ae8059 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts @@ -6,7 +6,7 @@ */ import { Dispatch, SetStateAction, useCallback } from 'react'; import { type DataView } from '@kbn/data-views-plugin/common'; -import { BoolQuery } from '@kbn/es-query'; +import { BoolQuery, Filter } from '@kbn/es-query'; import { CriteriaWithPagination } from '@elastic/eui'; import { DataTableRecord } from '@kbn/discover-utils/types'; import { useUrlQuery } from '../use_url_query'; @@ -21,7 +21,7 @@ export interface CloudPostureTableResult { sort: any; // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable filters: any[]; - query?: { bool: BoolQuery }; + query: { bool: BoolQuery }; queryError?: Error; pageIndex: number; // TODO: remove any, urlQuery is an object with query fields but we also add custom fields to it, need to assert usages @@ -48,12 +48,14 @@ export const useCloudPostureTable = ({ dataView, paginationLocalStorageKey, columnsLocalStorageKey, + additionalFilters, }: { // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable defaultQuery?: (params: any) => any; dataView: DataView; paginationLocalStorageKey: string; columnsLocalStorageKey?: string; + additionalFilters?: Filter; }): CloudPostureTableResult => { const getPersistedDefaultQuery = usePersistedQuery(defaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); @@ -117,6 +119,7 @@ export const useCloudPostureTable = ({ dataView, filters: urlQuery.filters, query: urlQuery.query, + ...(additionalFilters ? { additionalFilters: [additionalFilters] } : {}), }); const handleUpdateQuery = useCallback( @@ -133,12 +136,14 @@ export const useCloudPostureTable = ({ }) .flat() || []; + const queryError = baseEsQuery instanceof Error ? baseEsQuery : undefined; + return { setUrlQuery, sort: urlQuery.sort, filters: urlQuery.filters, query: baseEsQuery.query, - queryError: baseEsQuery.error, + queryError, pageIndex: urlQuery.pageIndex, urlQuery, setTableOptions, diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts index 2d3f9b1c7605c..0ce63a26a5dc1 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts @@ -23,15 +23,13 @@ const getBaseQuery = ({ filters, config, }: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { + console.log('filters', filters); try { return { query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query }; } catch (error) { - return { - query: undefined, - error: error instanceof Error ? error : new Error('Unknown Error'), - }; + throw new Error(error); } }; @@ -59,6 +57,7 @@ export const useBaseEsQuery = ({ dataView, filters, query, + additionalFilters, }: FindingsBaseURLQuery & FindingsBaseProps) => { const { notifications: { toasts }, @@ -70,8 +69,14 @@ export const useBaseEsQuery = ({ const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards'); const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]); const baseEsQuery = useMemo( - () => getBaseQuery({ dataView, filters, query, config }), - [dataView, filters, query, config] + () => + getBaseQuery({ + dataView, + filters: filters.concat(additionalFilters ?? []).flat(), + query, + config, + }), + [dataView, filters, additionalFilters, query, config] ); /** @@ -83,7 +88,7 @@ export const useBaseEsQuery = ({ }, [filters, filterManager, queryString, query]); const handleMalformedQueryError = () => { - const error = baseEsQuery.error; + const error = baseEsQuery instanceof Error ? baseEsQuery : undefined; if (error) { toasts.addError(error, { title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { @@ -94,7 +99,7 @@ export const useBaseEsQuery = ({ } }; - useEffect(handleMalformedQueryError, [baseEsQuery.error, toasts]); + useEffect(handleMalformedQueryError, [baseEsQuery, toasts]); return baseEsQuery; }; diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index 6ebfe7c7a0fa3..8617c74a03181 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -14,6 +14,11 @@ export type FindingsGroupByKind = 'default' | 'resource'; export interface FindingsBaseURLQuery { query: Query; filters: Filter[]; + /** + * Filters that are part of the query + * but not persisted in the URL or in the Filter Manager + */ + additionalFilters?: Filter[]; } export interface FindingsBaseProps { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index d0b8c5e90e3f6..cec257f839449 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -4,13 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataTableRecord } from '@kbn/discover-utils/types'; -import { Filter, Query } from '@kbn/es-query'; +import { buildEsQuery, Filter, Query } from '@kbn/es-query'; import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolution-grouping'; import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; +import { useUrlQuery } from '../../../common/hooks/use_url_query'; +import { + useBaseEsQuery, + usePersistedQuery, +} from '../../../common/hooks/use_cloud_posture_table/utils'; import { TimestampTableCell } from '../../../components/timestamp_table_cell'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import type { Evaluation } from '../../../../common/types'; @@ -118,14 +123,17 @@ export const FINDINGS_UNIT = (totalCount: number) => }); export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { - // const cloudPostureTable = useCloudPostureTable({ - // dataView, - // paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, - // columnsLocalStorageKey, - // defaultQuery: getDefaultQuery, - // }); + const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + + const [activePage, setActivePage] = useState(0); + const [pageSize, setPageSize] = useState(10); - // const { query, sort } = cloudPostureTable; + const baseEsQuery = useBaseEsQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + }); const grouping = useGrouping({ componentProps: { @@ -155,12 +163,12 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { fields: dataView.fields, groupingId: 'cspLatestFindings', maxGroupingLevels: 1, - // onGroupChange: (params) => { - // console.log('onGroupChange', params); - // }, - // onOptionsChange: (options) => { - // console.log('onOptionsChange', options); - // }, + onGroupChange: (params) => { + console.log('onGroupChange', params); + }, + onOptionsChange: (options) => { + console.log('onOptionsChange', options); + }, }); const selectedGroup = grouping.selectedGroups[0]; @@ -168,15 +176,15 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { console.log({ selectedGroup }); const groupingQuery = getGroupingQuery({ - additionalFilters: [], - from: 'now-1y', + additionalFilters: [baseEsQuery.query], groupByField: selectedGroup, uniqueValue: selectedGroup, + from: 'now-1y', to: 'now', - // pageNumber, + pageNumber: activePage, // rootAggregations, // runtimeMappings, - // size = DEFAULT_GROUP_BY_FIELD_SIZE, + size: pageSize, // sort, // statsAggregations, // to, @@ -203,27 +211,30 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { if (isNoneSelected) { return ( - + <> + + + ); } return ( <> + {grouping.getGrouping({ - activePage: 0, + activePage, data: aggs, groupingLevel: 0, inspectButton: undefined, isLoading: isFetching, - itemsPerPage: 10, - onChangeGroupsItemsPerPage: () => { - console.log('onChangeGroupsItemsPerPage'); + itemsPerPage: pageSize, + onChangeGroupsItemsPerPage: (size) => { + setPageSize(size); }, - onChangeGroupsPage: () => { - console.log('onChangeGroupsPage'); + onChangeGroupsPage: (index) => { + setActivePage(index); }, renderChildComponent: (groupFilter) => { - console.log({ groupFilter }); return ; }, onGroupClose: () => { @@ -243,15 +254,10 @@ export const LatestFindingsContainerTable = ({ dataView, filter }: FindingsBaseP paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, columnsLocalStorageKey, defaultQuery: getDefaultQuery, + additionalFilters: filter, }); - const { query, sort, queryError, setUrlQuery, getRowsFromPages } = cloudPostureTable; - - useEffect(() => { - setUrlQuery({ - filters: filter, - }); - }, [filter, setUrlQuery]); + const { query, sort, queryError, getRowsFromPages } = cloudPostureTable; const { data, @@ -271,7 +277,6 @@ export const LatestFindingsContainerTable = ({ dataView, filter }: FindingsBaseP return ( - {error && } {!error && ( @@ -340,7 +345,6 @@ export const LatestFindingsContainerOld = ({ dataView, groupSelector }: Findings return ( - {error && } {!error && ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx index af5f6fe62b395..7c6b347cf7ece 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx @@ -58,6 +58,7 @@ export interface FindingsGroupingAggregation { export const getGroupedFindingsQuery = (query: any) => ({ index: CSP_LATEST_FINDINGS_DATA_VIEW, ...query, + size: 0, }); export const useGroupedFindings = ({ query, enabled = true }: any) => { @@ -84,39 +85,7 @@ export const useGroupedFindings = ({ query, enabled = true }: any) => { 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('Failed to group by, missing resource id'); - - // const page = aggregations.resources.buckets.map(createFindingsByResource); - - // const aggs = parseGroupingQuery( - // // fallback to selectedGroup if queriedGroup.current is null, this happens in tests - // queriedGroup.current === null ? selectedGroup : queriedGroup.current, - // uniqueValue, - // alertsGroupsData?.aggregations - // ); - return aggregations; - // return { - // ...aggregations, - // groupByFields: { - // ...aggregations.groupByFields, - // buckets: [ - // ...aggregations.groupByFields.buckets.map((bucket) => ({ - // ...bucket, - // unitsCount: { - // value: 2, - // }, - // selectedGroup: 'resource.id', - // key_as_string: 'Vulnerability: CVE-2022-28734', - // key: ['Vulnerability: CVE-2022-28734'], - // })), - // ], - // }, - // }; }, { onError: (err: Error) => showErrorToast(toasts, err), From 4fd1510a4a3a7b7b04f30661ccdd22397ac6751b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 14:24:36 -0700 Subject: [PATCH 06/63] adding custom title --- .../src/hooks/use_get_group_selector.tsx | 3 +++ .../kbn-securitysolution-grouping/src/hooks/use_grouping.tsx | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index 7aede5c249ad0..b543bff5af449 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -30,6 +30,7 @@ export interface UseGetGroupSelectorArgs { event: string | string[], count?: number | undefined ) => void; + title?: string; } interface UseGetGroupSelectorStateless @@ -84,6 +85,7 @@ export const useGetGroupSelector = ({ onGroupChange, onOptionsChange, tracker, + title, }: UseGetGroupSelectorArgs) => { const { activeGroups: selectedGroups, options } = groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; @@ -188,6 +190,7 @@ export const useGetGroupSelector = ({ fields, maxGroupingLevels, options, + title, }} /> ); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index a744fc6c4ff7c..dfde9506ae4c2 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -72,6 +72,7 @@ interface GroupingArgs { event: string | string[], count?: number | undefined ) => void; + title?: string; } /** @@ -85,6 +86,7 @@ interface GroupingArgs { * @param onGroupChange callback executed when selected group is changed, used for tracking * @param onOptionsChange callback executed when grouping options are changed, used for consumer grouping selector * @param tracker telemetry handler + * @param title title of the grouping selector component * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroups } */ export const useGrouping = ({ @@ -96,6 +98,7 @@ export const useGrouping = ({ onGroupChange, onOptionsChange, tracker, + title, }: GroupingArgs): Grouping => { const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState); const { activeGroups: selectedGroups } = useMemo( @@ -125,6 +128,7 @@ export const useGrouping = ({ onGroupChange, onOptionsChange, tracker, + title, }); const getGrouping = useCallback( From fd4e071ca365e0c3eec5c575d7d952f63016f7fd Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 14:25:42 -0700 Subject: [PATCH 07/63] additional filters --- .../hooks/use_cloud_posture_table/use_cloud_posture_table.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts index c6de939ae8059..443d0d7215f3a 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts @@ -49,13 +49,14 @@ export const useCloudPostureTable = ({ paginationLocalStorageKey, columnsLocalStorageKey, additionalFilters, + pageIndexUrlSuffix = '', }: { // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable defaultQuery?: (params: any) => any; dataView: DataView; paginationLocalStorageKey: string; columnsLocalStorageKey?: string; - additionalFilters?: Filter; + additionalFilters?: Filter[]; }): CloudPostureTableResult => { const getPersistedDefaultQuery = usePersistedQuery(defaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); @@ -119,7 +120,7 @@ export const useCloudPostureTable = ({ dataView, filters: urlQuery.filters, query: urlQuery.query, - ...(additionalFilters ? { additionalFilters: [additionalFilters] } : {}), + ...(additionalFilters ? { additionalFilters } : {}), }); const handleUpdateQuery = useCallback( From 40a7797982c51a1aaf44396b3777a54060b942c2 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 14:26:08 -0700 Subject: [PATCH 08/63] datatable group component --- .../additional_controls.tsx | 14 +++----------- .../cloud_security_data_table.tsx | 12 +++++++++++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx index 3ed680c4d8317..6b9e48e7738e5 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx @@ -10,9 +10,7 @@ import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; import { type DataView } from '@kbn/data-views-plugin/common'; import numeral from '@elastic/numeral'; import { FieldsSelectorModal } from './fields_selector'; -import { FindingsGroupBySelector } from '../../pages/configurations/layout/findings_group_by_selector'; import { useStyles } from './use_styles'; -import { AnyAsyncThunk } from '@reduxjs/toolkit/dist/matchers'; const formatNumber = (value: number) => { return value < 1000 ? value : numeral(value).format('0.0a'); @@ -33,7 +31,7 @@ export const AdditionalControls = ({ columns: string[]; onAddColumn: (column: string) => void; onRemoveColumn: (column: string) => void; - groupSelector: any; + groupSelector?: JSX.Element; }) => { const styles = useStyles(); @@ -43,9 +41,6 @@ export const AdditionalControls = ({ const showModal = () => setIsFieldSelectorModalVisible(true); const GroupSelector = () => { - if (groupSelector === null) { - return null; - } if (groupSelector) { return ( @@ -53,11 +48,8 @@ export const AdditionalControls = ({ ); } - return ( - - ; - - ); + + return null; }; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx index 6aac11bd78ed5..f83df97e8deab 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx @@ -73,6 +73,15 @@ interface CloudSecurityDataGridProps { */ loadMore: () => void; 'data-test-subj'?: string; + /** + * This is the component that will be rendered in the group selector. + * This component will receive the current group and a function to change the group. + */ + groupSelector?: JSX.Element; + /** + * Height override for the data grid. + */ + height?: number; } export const CloudSecurityDataTable = ({ @@ -87,6 +96,7 @@ export const CloudSecurityDataTable = ({ title, customCellRenderer, groupSelector, + height, ...rest }: CloudSecurityDataGridProps) => { const { @@ -222,7 +232,7 @@ export const CloudSecurityDataTable = ({ // Change the height of the grid to fit the page // If there are filters, leave space for the filter bar // Todo: Replace this component with EuiAutoSizer - height: `calc(100vh - ${filters.length > 0 ? 443 : 403}px)`, + height: height ?? `calc(100vh - ${filters.length > 0 ? 443 : 403}px)`, }; const rowHeightState = From bd80f343ed2ee579df93cb78ebc41d3752cd011f Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 14:26:45 -0700 Subject: [PATCH 09/63] clean up code --- .../latest_findings_container.tsx | 169 +++++++++++------- .../latest_findings/use_grouped_findings.tsx | 37 +--- 2 files changed, 108 insertions(+), 98 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index cec257f839449..47fc9a783ca89 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -8,9 +8,10 @@ import React, { useEffect, useMemo, useState } from 'react'; import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataTableRecord } from '@kbn/discover-utils/types'; -import { buildEsQuery, Filter, Query } from '@kbn/es-query'; +import { Filter, Query } from '@kbn/es-query'; import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolution-grouping'; import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; +import * as uuid from 'uuid'; import { useUrlQuery } from '../../../common/hooks/use_url_query'; import { useBaseEsQuery, @@ -34,7 +35,6 @@ import { CloudSecurityDefaultColumn, } from '../../../components/cloud_security_data_table'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; -// import { useFindingsByResource } from '../latest_findings_by_resource/use_findings_by_resource'; import { useGroupedFindings } from './use_grouped_findings'; const getDefaultQuery = ({ @@ -122,98 +122,102 @@ export const FINDINGS_UNIT = (totalCount: number) => defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`, }); +const defaultGroupingOptions = [ + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { + defaultMessage: 'Resource', + }), + key: 'resource.name', + }, + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { + defaultMessage: 'Rule name', + }), + key: 'rule.name', + }, + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { + defaultMessage: 'Cloud account', + }), + key: 'cloud.account.name', + }, + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', { + defaultMessage: 'Kubernetes cluster', + }), + key: 'orchestrator.cluster.name', + }, +]; + +const groupingTitle = i18n.translate('xpack.csp.findings.latestFindings.groupBy', { + defaultMessage: 'Group findings by', +}); + export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const [activePage, setActivePage] = useState(0); const [pageSize, setPageSize] = useState(10); - const baseEsQuery = useBaseEsQuery({ + const { query } = useBaseEsQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query, }); + /** + * Reset the active page when the filters or query change + * This is needed because the active page is not automatically reset when the filters or query change + */ + useEffect(() => { + setActivePage(0); + }, [urlQuery.filters, urlQuery.query]); + const grouping = useGrouping({ componentProps: { - // groupPanelRenderer: renderGroupPanel, - // groupStatsRenderer: getStats, - // onGroupToggle, unit: FINDINGS_UNIT, }, - defaultGroupingOptions: [ - { - label: 'Resource', - key: 'resource.id', - }, - { - label: 'Rule name', - key: 'rule.name', - }, - { - label: 'Cloud account', - key: 'cloud.account.name', - }, - { - label: 'Kubernetes cluster', - key: 'orchestrator.cluster.name', - }, - ], + defaultGroupingOptions, fields: dataView.fields, groupingId: 'cspLatestFindings', maxGroupingLevels: 1, - onGroupChange: (params) => { - console.log('onGroupChange', params); - }, - onOptionsChange: (options) => { - console.log('onOptionsChange', options); + title: groupingTitle, + onGroupChange: () => { + setActivePage(0); }, }); const selectedGroup = grouping.selectedGroups[0]; const isNoneSelected = isNoneGroup(grouping.selectedGroups); - console.log({ selectedGroup }); + + const uniqueValue = useMemo(() => `${selectedGroup}-${uuid.v4()}`, [selectedGroup]); const groupingQuery = getGroupingQuery({ - additionalFilters: [baseEsQuery.query], + additionalFilters: [query], groupByField: selectedGroup, - uniqueValue: selectedGroup, + uniqueValue, from: 'now-1y', to: 'now', - pageNumber: activePage, - // rootAggregations, - // runtimeMappings, + pageNumber: activePage * pageSize, size: pageSize, - // sort, - // statsAggregations, - // to, - // uniqueValue, + sort: [{ _key: { order: 'asc' } }], }); const { data, isFetching } = useGroupedFindings({ - // sortDirection: cloudPostureTable.urlQuery.sort.direction, query: groupingQuery, enabled: !isNoneSelected, }); const aggs = useMemo( - // queriedGroup because `selectedGroup` updates before the query response - () => - parseGroupingQuery( - // fallback to selectedGroup if queriedGroup.current is null, this happens in tests - selectedGroup, - selectedGroup, - data - ), - [data, selectedGroup] + () => parseGroupingQuery(selectedGroup, uniqueValue, data), + [data, selectedGroup, uniqueValue] ); if (isNoneSelected) { return ( <> - + ); } @@ -235,20 +239,29 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { setActivePage(index); }, renderChildComponent: (groupFilter) => { - return ; - }, - onGroupClose: () => { - console.log('onGroupClose'); + return ( + + ); }, + onGroupClose: () => {}, selectedGroup, - // selectedGroup: 'resource.id', takeActionItems: () => [], })} ); }; -export const LatestFindingsContainerTable = ({ dataView, filter }: FindingsBaseProps) => { +export const LatestFindingsTable = ({ + dataView, + filter, +}: FindingsBaseProps & { + filter: Filter[]; +}) => { const cloudPostureTable = useCloudPostureTable({ dataView, paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, @@ -294,7 +307,7 @@ export const LatestFindingsContainerTable = ({ dataView, filter }: FindingsBaseP loadMore={fetchNextPage} title={title} customCellRenderer={customCellRenderer} - groupSelector={null} + height={512} /> )} @@ -302,12 +315,24 @@ export const LatestFindingsContainerTable = ({ dataView, filter }: FindingsBaseP ); }; -export const LatestFindingsContainerOld = ({ dataView, groupSelector }: FindingsBaseProps) => { +export const LatestFindingsContainerTable = ({ + dataView, + groupSelector, + height, + showDistributionBar = true, + additionalFilters, +}: FindingsBaseProps & { + groupSelector?: JSX.Element; + height?: number; + showDistributionBar?: boolean; + additionalFilters?: Filter[]; +}) => { const cloudPostureTable = useCloudPostureTable({ dataView, paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, columnsLocalStorageKey, defaultQuery: getDefaultQuery, + additionalFilters, }); const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureTable; @@ -345,18 +370,25 @@ export const LatestFindingsContainerOld = ({ dataView, groupSelector }: Findings return ( - - {error && } + {error && ( + <> + + + + )} {!error && ( <> - {total > 0 && ( - + {showDistributionBar && total > 0 && ( + <> + + + + )} - )} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx index 7c6b347cf7ece..bd529cede5ca6 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx @@ -8,7 +8,7 @@ import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import { GroupingAggregation } from '@kbn/securitysolution-grouping'; -import { GenericBuckets } from '@kbn/securitysolution-grouping/src'; +import { GenericBuckets, GroupingQuery } from '@kbn/securitysolution-grouping/src'; import { useQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; @@ -22,42 +22,17 @@ export interface FindingsGroupingAggregation { unitsCount?: { value?: NumberOrNull; }; - description?: { - buckets?: GenericBuckets[]; - }; - severitiesSubAggregation?: { - buckets?: GenericBuckets[]; - }; - countSeveritySubAggregation?: { - value?: NumberOrNull; - }; - usersCountAggregation?: { - value?: NumberOrNull; - }; - hostsCountAggregation?: { + groupsCount?: { value?: NumberOrNull; }; - ipsCountAggregation?: { - value?: NumberOrNull; - }; - rulesCountAggregation?: { - value?: NumberOrNull; - }; - ruleTags?: { - doc_count_error_upper_bound?: number; - sum_other_doc_count?: number; + groupByFields?: { buckets?: GenericBuckets[]; }; - stackByMultipleFields1?: { - buckets?: GenericBuckets[]; - doc_count_error_upper_bound?: number; - sum_other_doc_count?: number; - }; } -export const getGroupedFindingsQuery = (query: any) => ({ - index: CSP_LATEST_FINDINGS_DATA_VIEW, +export const getGroupedFindingsQuery = (query: GroupingQuery) => ({ ...query, + index: CSP_LATEST_FINDINGS_DATA_VIEW, size: 0, }); @@ -90,6 +65,8 @@ export const useGroupedFindings = ({ query, enabled = true }: any) => { { onError: (err: Error) => showErrorToast(toasts, err), enabled, + // This allows the UI to keep the previous data while the new data is being fetched + keepPreviousData: true, } ); }; From befa48990ac644b47735662f8a1eb01e3e5e6442 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 25 Oct 2023 21:34:37 +0000 Subject: [PATCH 10/63] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/cloud_security_posture/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/tsconfig.json b/x-pack/plugins/cloud_security_posture/tsconfig.json index 113ddcb92202a..592e4139e8492 100755 --- a/x-pack/plugins/cloud_security_posture/tsconfig.json +++ b/x-pack/plugins/cloud_security_posture/tsconfig.json @@ -60,7 +60,8 @@ "@kbn/ui-actions-plugin", "@kbn/core-http-server-mocks", "@kbn/field-formats-plugin", - "@kbn/data-view-field-editor-plugin" + "@kbn/data-view-field-editor-plugin", + "@kbn/securitysolution-grouping" ], "exclude": ["target/**/*"] } From 2f761d50bf3954b3dea411d1c9c82e86a0253463 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 14:43:05 -0700 Subject: [PATCH 11/63] add more information to disabled --- .../src/components/group_selector/index.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx index 84532a3802985..162e4e81a49e3 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx @@ -43,8 +43,17 @@ const GroupSelectorComponent = ({ [groupsSelected] ); - const panels: EuiContextMenuPanelDescriptor[] = useMemo( - () => [ + const panels: EuiContextMenuPanelDescriptor[] = useMemo(() => { + const isOptionDisabled = (key?: string) => { + // Do not disable when maxGroupingLevels is 1 to allow toggling between groups + if (maxGroupingLevels === 1) { + return false; + } + // Disable all non selected options when the maxGroupingLevels is reached + return groupsSelected.length === maxGroupingLevels && (key ? !isGroupSelected(key) : true); + }; + + return [ { id: 'firstPanel', title: i18n.SELECT_FIELD(maxGroupingLevels), @@ -57,10 +66,7 @@ const GroupSelectorComponent = ({ }, ...options.map((o) => ({ 'data-test-subj': `panel-${o.key}`, - disabled: - maxGroupingLevels > 1 && - groupsSelected.length === maxGroupingLevels && - !isGroupSelected(o.key), + disabled: isOptionDisabled(o.key), name: o.label, onClick: () => onGroupChange(o.key), icon: isGroupSelected(o.key) ? 'check' : 'empty', @@ -69,7 +75,7 @@ const GroupSelectorComponent = ({ 'data-test-subj': `panel-custom`, name: i18n.CUSTOM_FIELD, icon: 'empty', - disabled: maxGroupingLevels > 1 && groupsSelected.length === maxGroupingLevels, + disabled: isOptionDisabled(), panel: 'customPanel', hasPanel: true, }, @@ -90,9 +96,8 @@ const GroupSelectorComponent = ({ /> ), }, - ], - [fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options] - ); + ]; + }, [fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options]); const selectedOptions = useMemo( () => options.filter((groupOption) => isGroupSelected(groupOption.key)), [isGroupSelected, options] From ec0dbdfbdd58f24ad831361a631681c0845467f4 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 22:25:36 -0700 Subject: [PATCH 12/63] code clean up --- .../src/hooks/use_get_group_selector.tsx | 1 + .../kbn-securitysolution-grouping/src/hooks/use_grouping.tsx | 2 +- .../hooks/use_cloud_posture_table/use_cloud_posture_table.ts | 1 - .../public/common/hooks/use_cloud_posture_table/utils.ts | 1 - .../cloud_security_posture/public/components/empty_state.tsx | 4 +++- .../latest_findings/latest_findings_container.tsx | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index b543bff5af449..573c5280667b5 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -122,6 +122,7 @@ export const useGetGroupSelector = ({ return; } + // Simulate a toggle behavior when maxGroupingLevels is 1 if (maxGroupingLevels === 1) { setSelectedGroups([groupSelection]); } else { diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index dfde9506ae4c2..997dd0895e35e 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -86,7 +86,7 @@ interface GroupingArgs { * @param onGroupChange callback executed when selected group is changed, used for tracking * @param onOptionsChange callback executed when grouping options are changed, used for consumer grouping selector * @param tracker telemetry handler - * @param title title of the grouping selector component + * @param title of the grouping selector component * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroups } */ export const useGrouping = ({ diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts index 443d0d7215f3a..7172adbf26947 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts @@ -49,7 +49,6 @@ export const useCloudPostureTable = ({ paginationLocalStorageKey, columnsLocalStorageKey, additionalFilters, - pageIndexUrlSuffix = '', }: { // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable defaultQuery?: (params: any) => any; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts index 0ce63a26a5dc1..292a0007f519a 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts @@ -23,7 +23,6 @@ const getBaseQuery = ({ filters, config, }: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { - console.log('filters', filters); try { return { query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query diff --git a/x-pack/plugins/cloud_security_posture/public/components/empty_state.tsx b/x-pack/plugins/cloud_security_posture/public/components/empty_state.tsx index 9c38e635062f7..43f39023c9c32 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/empty_state.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/empty_state.tsx @@ -30,7 +30,9 @@ export const EmptyState = ({ && > .euiEmptyPrompt__main { gap: ${euiTheme.size.xl}; } - margin-top: ${euiTheme.size.xxxl}}; + && { + margin-top: ${euiTheme.size.xxxl}}; + } `} data-test-subj={EMPTY_STATE_TEST_SUBJ} icon={ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 47fc9a783ca89..3c7ab70286b29 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -54,7 +54,7 @@ const getDefaultQuery = ({ const defaultColumns: CloudSecurityDefaultColumn[] = [ { id: 'result.evaluation', width: 80 }, { id: 'resource.id' }, - { id: 'resource.id' }, + { id: 'resource.name' }, { id: 'resource.sub_type' }, { id: 'rule.benchmark.rule_number' }, { id: 'rule.name' }, From d55081c479da438c2e7c23f8494d5546603a5b56 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 25 Oct 2023 22:31:48 -0700 Subject: [PATCH 13/63] hiding take action button --- .../latest_findings/latest_findings_container.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 3c7ab70286b29..aa9299177ba5a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -12,6 +12,7 @@ import { Filter, Query } from '@kbn/es-query'; import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolution-grouping'; import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; import * as uuid from 'uuid'; +import { css } from '@emotion/react'; import { useUrlQuery } from '../../../common/hooks/use_url_query'; import { useBaseEsQuery, @@ -223,7 +224,13 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { } return ( - <> +
{grouping.getGrouping({ activePage, @@ -252,7 +259,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { selectedGroup, takeActionItems: () => [], })} - +
); }; From 59cf7e39daea154c3d87d8e4bb7ac762a4f3c461 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 10 Nov 2023 18:27:46 -0800 Subject: [PATCH 14/63] addressing PR review suggestions --- .../src/components/group_selector/index.tsx | 3 +- .../src/components/translations.ts | 4 ++ .../src/hooks/use_get_group_selector.tsx | 20 ++++----- .../public/common/types.ts | 3 +- .../additional_controls.tsx | 32 +++++++-------- .../cloud_security_data_table.tsx | 6 +-- .../latest_findings_container.tsx | 41 +++++++++++-------- .../latest_findings/use_grouped_findings.tsx | 8 +++- 8 files changed, 67 insertions(+), 50 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx index 162e4e81a49e3..ffec5db362066 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx @@ -56,7 +56,8 @@ const GroupSelectorComponent = ({ return [ { id: 'firstPanel', - title: i18n.SELECT_FIELD(maxGroupingLevels), + title: + maxGroupingLevels === 1 ? i18n.SELECT_SINGLE_FIELD : i18n.SELECT_FIELD(maxGroupingLevels), items: [ { 'data-test-subj': 'panel-none', diff --git a/packages/kbn-securitysolution-grouping/src/components/translations.ts b/packages/kbn-securitysolution-grouping/src/components/translations.ts index c2e0f13e325a5..de5337db904da 100644 --- a/packages/kbn-securitysolution-grouping/src/components/translations.ts +++ b/packages/kbn-securitysolution-grouping/src/components/translations.ts @@ -32,6 +32,10 @@ export const SELECT_FIELD = (groupingLevelsCount: number) => defaultMessage: 'Select up to {groupingLevelsCount} groupings', }); +export const SELECT_SINGLE_FIELD = i18n.translate('grouping.groupBySingleField', { + defaultMessage: 'Select grouping', +}); + export const NONE = i18n.translate('grouping.noneGroupByOptionName', { defaultMessage: 'None', }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index 573c5280667b5..d3e518a48fe59 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -112,20 +112,20 @@ export const useGetGroupSelector = ({ const onChange = useCallback( (groupSelection: string) => { - if (selectedGroups.find((selected) => selected === groupSelection)) { - const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection); - if (groups.length === 0) { - setSelectedGroups(['none']); - } else { - setSelectedGroups(groups); - } - return; - } - // Simulate a toggle behavior when maxGroupingLevels is 1 if (maxGroupingLevels === 1) { setSelectedGroups([groupSelection]); } else { + if (selectedGroups.find((selected) => selected === groupSelection)) { + const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection); + if (groups.length === 0) { + setSelectedGroups(['none']); + } else { + setSelectedGroups(groups); + } + return; + } + const newSelectedGroups = isNoneGroup([groupSelection]) ? [groupSelection] : [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection]; diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index 8617c74a03181..d35127a410a09 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -15,8 +15,7 @@ export interface FindingsBaseURLQuery { query: Query; filters: Filter[]; /** - * Filters that are part of the query - * but not persisted in the URL or in the Filter Manager + * Filters that are part of the query but not persisted in the URL or in the Filter Manager */ additionalFilters?: Filter[]; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx index 6b9e48e7738e5..b1f79779e65e4 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx @@ -16,6 +16,16 @@ const formatNumber = (value: number) => { return value < 1000 ? value : numeral(value).format('0.0a'); }; +const GroupSelectorWrapper: React.FC = ({ children }) => { + const styles = useStyles(); + + return ( + + {children} + + ); +}; + export const AdditionalControls = ({ total, title, @@ -23,7 +33,7 @@ export const AdditionalControls = ({ columns, onAddColumn, onRemoveColumn, - groupSelector, + groupSelectorComponent, }: { total: number; title: string; @@ -31,27 +41,13 @@ export const AdditionalControls = ({ columns: string[]; onAddColumn: (column: string) => void; onRemoveColumn: (column: string) => void; - groupSelector?: JSX.Element; + groupSelectorComponent?: JSX.Element; }) => { - const styles = useStyles(); - const [isFieldSelectorModalVisible, setIsFieldSelectorModalVisible] = useState(false); const closeModal = () => setIsFieldSelectorModalVisible(false); const showModal = () => setIsFieldSelectorModalVisible(true); - const GroupSelector = () => { - if (groupSelector) { - return ( - - {groupSelector} - - ); - } - - return null; - }; - return ( <> {isFieldSelectorModalVisible && ( @@ -79,7 +75,9 @@ export const AdditionalControls = ({ })}
- + {groupSelectorComponent && ( + {groupSelectorComponent} + )} ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx index 34a9f14562968..71d18342890c3 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx @@ -73,7 +73,7 @@ interface CloudSecurityDataGridProps { * This is the component that will be rendered in the group selector. * This component will receive the current group and a function to change the group. */ - groupSelector?: JSX.Element; + groupSelectorComponent?: JSX.Element; /** * Height override for the data grid. */ @@ -91,7 +91,7 @@ export const CloudSecurityDataTable = ({ loadMore, title, customCellRenderer, - groupSelector, + groupSelectorComponent, height, ...rest }: CloudSecurityDataGridProps) => { @@ -220,7 +220,7 @@ export const CloudSecurityDataTable = ({ columns={currentColumns} onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} - groupSelector={groupSelector} + groupSelectorComponent={groupSelectorComponent} /> ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index aa9299177ba5a..6e44e751bd484 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -13,6 +13,7 @@ import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolutio import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; import * as uuid from 'uuid'; import { css } from '@emotion/react'; +import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; import { useUrlQuery } from '../../../common/hooks/use_url_query'; import { useBaseEsQuery, @@ -154,11 +155,16 @@ const groupingTitle = i18n.translate('xpack.csp.findings.latestFindings.groupBy' defaultMessage: 'Group findings by', }); +const DEFAULT_PAGE_SIZE = 10; +const DEFAULT_TABLE_HEIGHT = 512; +const GROUPING_ID = 'cspLatestFindings'; +const MAX_GROUPING_LEVELS = 1; + export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const [activePage, setActivePage] = useState(0); - const [pageSize, setPageSize] = useState(10); + const [activePageIndex, setActivePageIndex] = useState(0); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const { query } = useBaseEsQuery({ dataView, @@ -171,7 +177,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { * This is needed because the active page is not automatically reset when the filters or query change */ useEffect(() => { - setActivePage(0); + setActivePageIndex(0); }, [urlQuery.filters, urlQuery.query]); const grouping = useGrouping({ @@ -180,11 +186,11 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { }, defaultGroupingOptions, fields: dataView.fields, - groupingId: 'cspLatestFindings', - maxGroupingLevels: 1, + groupingId: GROUPING_ID, + maxGroupingLevels: MAX_GROUPING_LEVELS, title: groupingTitle, onGroupChange: () => { - setActivePage(0); + setActivePageIndex(0); }, }); @@ -197,9 +203,9 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { additionalFilters: [query], groupByField: selectedGroup, uniqueValue, - from: 'now-1y', + from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, to: 'now', - pageNumber: activePage * pageSize, + pageNumber: activePageIndex * pageSize, size: pageSize, sort: [{ _key: { order: 'asc' } }], }); @@ -218,7 +224,10 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { return ( <> - + ); } @@ -233,7 +242,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { > {grouping.getGrouping({ - activePage, + activePage: activePageIndex, data: aggs, groupingLevel: 0, inspectButton: undefined, @@ -243,14 +252,14 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { setPageSize(size); }, onChangeGroupsPage: (index) => { - setActivePage(index); + setActivePageIndex(index); }, renderChildComponent: (groupFilter) => { return ( ); @@ -314,7 +323,7 @@ export const LatestFindingsTable = ({ loadMore={fetchNextPage} title={title} customCellRenderer={customCellRenderer} - height={512} + height={DEFAULT_TABLE_HEIGHT} /> )} @@ -324,12 +333,12 @@ export const LatestFindingsTable = ({ export const LatestFindingsContainerTable = ({ dataView, - groupSelector, + groupSelectorComponent, height, showDistributionBar = true, additionalFilters, }: FindingsBaseProps & { - groupSelector?: JSX.Element; + groupSelectorComponent?: JSX.Element; height?: number; showDistributionBar?: boolean; additionalFilters?: Filter[]; @@ -408,7 +417,7 @@ export const LatestFindingsContainerTable = ({ loadMore={fetchNextPage} title={title} customCellRenderer={customCellRenderer} - groupSelector={groupSelector} + groupSelectorComponent={groupSelectorComponent} height={height} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx index bd529cede5ca6..66b1faf5060c9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx @@ -36,7 +36,13 @@ export const getGroupedFindingsQuery = (query: GroupingQuery) => ({ size: 0, }); -export const useGroupedFindings = ({ query, enabled = true }: any) => { +export const useGroupedFindings = ({ + query, + enabled = true, +}: { + query: GroupingQuery; + enabled: boolean; +}) => { const { data, notifications: { toasts }, From 5abda9858eea998f76ca27fe22c14f5a39815ee3 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sun, 12 Nov 2023 20:14:58 -0800 Subject: [PATCH 15/63] splitting and optimizing code --- .../common/constants.ts | 2 +- .../cloud_security_grouping.tsx | 60 +++++ .../cloud_security_grouping/index.ts | 9 + .../use_cloud_security_grouping.ts | 92 +++++++ .../latest_findings/constants.ts | 71 ++++++ .../latest_findings_container.tsx | 228 ++++++------------ 6 files changed, 302 insertions(+), 160 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts create mode 100644 x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 1cc356cbfd5e3..5987881aed444 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -45,7 +45,7 @@ export const LATEST_FINDINGS_INDEX_TEMPLATE_NAME = 'logs-cloud_security_posture. export const LATEST_FINDINGS_INDEX_PATTERN = 'logs-cloud_security_posture.findings_latest-*'; export const LATEST_FINDINGS_INDEX_DEFAULT_NS = 'logs-cloud_security_posture.findings_latest-default'; -export const LATEST_FINDINGS_RETENTION_POLICY = '26h'; +export const LATEST_FINDINGS_RETENTION_POLICY = '26y'; export const BENCHMARK_SCORE_INDEX_TEMPLATE_NAME = 'logs-cloud_security_posture.scores'; export const BENCHMARK_SCORE_INDEX_PATTERN = 'logs-cloud_security_posture.scores-*'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx new file mode 100644 index 0000000000000..00b96e77d6402 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx @@ -0,0 +1,60 @@ +/* + * 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 { useGrouping } from '@kbn/securitysolution-grouping'; +import { ParsedGroupingAggregation } from '@kbn/securitysolution-grouping/src'; +import { Filter } from '@kbn/es-query'; +import React from 'react'; +import { css } from '@emotion/react'; + +interface CloudSecurityGroupingProps { + data: ParsedGroupingAggregation; + renderChildComponent: (groupFilter: Filter[]) => JSX.Element; + grouping: ReturnType; + activePageIndex: number; + isFetching: boolean; + pageSize: number; + onChangeGroupsItemsPerPage: (size: number) => void; + onChangeGroupsPage: (index: number) => void; + selectedGroup: string; +} + +export const CloudSecurityGrouping = ({ + data, + renderChildComponent, + grouping, + activePageIndex, + isFetching, + pageSize, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + selectedGroup, +}: CloudSecurityGroupingProps) => { + return ( +
+ {grouping.getGrouping({ + activePage: activePageIndex, + data, + groupingLevel: 0, + inspectButton: undefined, + isLoading: isFetching, + itemsPerPage: pageSize, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + renderChildComponent, + onGroupClose: () => {}, + selectedGroup, + takeActionItems: () => [], + })} +
+ ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts new file mode 100644 index 0000000000000..35a321d06119d --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { useCloudSecurityGrouping } from './use_cloud_security_grouping'; +export { CloudSecurityGrouping } from './cloud_security_grouping'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts new file mode 100644 index 0000000000000..1235291e62515 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -0,0 +1,92 @@ +/* + * 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 { useEffect, useMemo, useState } from 'react'; +import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; +import * as uuid from 'uuid'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { useUrlQuery } from '../../common/hooks/use_url_query'; +import { + useBaseEsQuery, + usePersistedQuery, +} from '../../common/hooks/use_cloud_posture_table/utils'; +import { FindingsBaseURLQuery } from '../../common/types'; + +const DEFAULT_PAGE_SIZE = 10; +const GROUPING_ID = 'cspLatestFindings'; +const MAX_GROUPING_LEVELS = 1; + +export const useCloudSecurityGrouping = ({ + dataView, + groupingTitle, + defaultGroupingOptions, + getDefaultQuery, + unit, +}: { + dataView: DataView; + groupingTitle: string; + defaultGroupingOptions: Array<{ label: string; key: string }>; + getDefaultQuery: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; + unit: (count: number) => string; +}) => { + const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + const [activePageIndex, setActivePageIndex] = useState(0); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + + const { query } = useBaseEsQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + }); + + /** + * Reset the active page when the filters or query change + * This is needed because the active page is not automatically reset when the filters or query change + */ + useEffect(() => { + setActivePageIndex(0); + }, [urlQuery.filters, urlQuery.query]); + + const grouping = useGrouping({ + componentProps: { + unit, + }, + defaultGroupingOptions, + fields: dataView.fields, + groupingId: GROUPING_ID, + maxGroupingLevels: MAX_GROUPING_LEVELS, + title: groupingTitle, + onGroupChange: () => { + setActivePageIndex(0); + }, + }); + + const selectedGroup = grouping.selectedGroups[0]; + const isNoneSelected = isNoneGroup(grouping.selectedGroups); + + const uniqueValue = useMemo(() => `${selectedGroup}-${uuid.v4()}`, [selectedGroup]); + + const onChangeGroupsItemsPerPage = (size: number) => { + setActivePageIndex(0); + setPageSize(size); + }; + + const onChangeGroupsPage = (index: number) => setActivePageIndex(index); + + return { + activePageIndex, + grouping, + pageSize, + query, + selectedGroup, + setUrlQuery, + uniqueValue, + isNoneSelected, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts new file mode 100644 index 0000000000000..54884856fccf1 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -0,0 +1,71 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FindingsBaseURLQuery } from '../../../common/types'; +import { CloudSecurityDefaultColumn } from '../../../components/cloud_security_data_table'; + +export const FINDINGS_UNIT = (totalCount: number) => + i18n.translate('xpack.csp.findings.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`, + }); + +export const defaultGroupingOptions = [ + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { + defaultMessage: 'Resource', + }), + key: 'resource.name', + }, + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { + defaultMessage: 'Rule name', + }), + key: 'rule.name', + }, + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { + defaultMessage: 'Cloud account', + }), + key: 'cloud.account.name', + }, + { + label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', { + defaultMessage: 'Kubernetes cluster', + }), + key: 'orchestrator.cluster.name', + }, +]; + +export const groupingTitle = i18n.translate('xpack.csp.findings.latestFindings.groupBy', { + defaultMessage: 'Group findings by', +}); + +export const DEFAULT_TABLE_HEIGHT = 512; + +export const getDefaultQuery = ({ + query, + filters, +}: FindingsBaseURLQuery): FindingsBaseURLQuery & { + sort: string[][]; +} => ({ + query, + filters, + sort: [['@timestamp', 'desc']], +}); + +export const defaultColumns: CloudSecurityDefaultColumn[] = [ + { id: 'result.evaluation', width: 80 }, + { id: 'resource.id' }, + { id: 'resource.name' }, + { id: 'resource.sub_type' }, + { id: 'rule.benchmark.rule_number' }, + { id: 'rule.name' }, + { id: 'rule.section' }, + { id: '@timestamp' }, +]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 6e44e751bd484..b79c5e1441e5d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -4,25 +4,22 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataTableRecord } from '@kbn/discover-utils/types'; -import { Filter, Query } from '@kbn/es-query'; -import { isNoneGroup, useGrouping, getGroupingQuery } from '@kbn/securitysolution-grouping'; +import { Filter } from '@kbn/es-query'; +import { getGroupingQuery } from '@kbn/securitysolution-grouping'; import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; -import * as uuid from 'uuid'; -import { css } from '@emotion/react'; -import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; -import { useUrlQuery } from '../../../common/hooks/use_url_query'; import { - useBaseEsQuery, - usePersistedQuery, -} from '../../../common/hooks/use_cloud_posture_table/utils'; + CloudSecurityGrouping, + useCloudSecurityGrouping, +} from '../../../components/cloud_security_grouping'; +import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; import { TimestampTableCell } from '../../../components/timestamp_table_cell'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import type { Evaluation } from '../../../../common/types'; -import type { FindingsBaseProps, FindingsBaseURLQuery } from '../../../common/types'; +import type { FindingsBaseProps } from '../../../common/types'; import { FindingsSearchBar } from '../layout/findings_search_bar'; import * as TEST_SUBJECTS from '../test_subjects'; import { useLatestFindings } from './use_latest_findings'; @@ -32,37 +29,17 @@ import { ErrorCallout } from '../layout/error_callout'; import { LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY } from '../../../common/constants'; import { CspFinding } from '../../../../common/schemas/csp_finding'; import { useCloudPostureTable } from '../../../common/hooks/use_cloud_posture_table'; -import { - CloudSecurityDataTable, - CloudSecurityDefaultColumn, -} from '../../../components/cloud_security_data_table'; +import { CloudSecurityDataTable } from '../../../components/cloud_security_data_table'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; import { useGroupedFindings } from './use_grouped_findings'; - -const getDefaultQuery = ({ - query, - filters, -}: { - query: Query; - filters: Filter[]; -}): FindingsBaseURLQuery & { - sort: string[][]; -} => ({ - query, - filters, - sort: [['@timestamp', 'desc']], -}); - -const defaultColumns: CloudSecurityDefaultColumn[] = [ - { id: 'result.evaluation', width: 80 }, - { id: 'resource.id' }, - { id: 'resource.name' }, - { id: 'resource.sub_type' }, - { id: 'rule.benchmark.rule_number' }, - { id: 'rule.name' }, - { id: 'rule.section' }, - { id: '@timestamp' }, -]; +import { + FINDINGS_UNIT, + groupingTitle, + defaultGroupingOptions, + getDefaultQuery, + DEFAULT_TABLE_HEIGHT, + defaultColumns, +} from './constants'; /** * Type Guard for checking if the given source is a CspFinding @@ -118,87 +95,26 @@ const customCellRenderer = (rows: DataTableRecord[]) => ({ ), }); -export const FINDINGS_UNIT = (totalCount: number) => - i18n.translate('xpack.csp.findings.unit', { - values: { totalCount }, - defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`, - }); - -const defaultGroupingOptions = [ - { - label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { - defaultMessage: 'Resource', - }), - key: 'resource.name', - }, - { - label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { - defaultMessage: 'Rule name', - }), - key: 'rule.name', - }, - { - label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { - defaultMessage: 'Cloud account', - }), - key: 'cloud.account.name', - }, - { - label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', { - defaultMessage: 'Kubernetes cluster', - }), - key: 'orchestrator.cluster.name', - }, -]; - -const groupingTitle = i18n.translate('xpack.csp.findings.latestFindings.groupBy', { - defaultMessage: 'Group findings by', -}); - -const DEFAULT_PAGE_SIZE = 10; -const DEFAULT_TABLE_HEIGHT = 512; -const GROUPING_ID = 'cspLatestFindings'; -const MAX_GROUPING_LEVELS = 1; - export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { - const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); - const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const [activePageIndex, setActivePageIndex] = useState(0); - const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - - const { query } = useBaseEsQuery({ + const { + activePageIndex, + grouping, + pageSize, + query, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + uniqueValue, + isNoneSelected, + } = useCloudSecurityGrouping({ dataView, - filters: urlQuery.filters, - query: urlQuery.query, - }); - - /** - * Reset the active page when the filters or query change - * This is needed because the active page is not automatically reset when the filters or query change - */ - useEffect(() => { - setActivePageIndex(0); - }, [urlQuery.filters, urlQuery.query]); - - const grouping = useGrouping({ - componentProps: { - unit: FINDINGS_UNIT, - }, + groupingTitle, defaultGroupingOptions, - fields: dataView.fields, - groupingId: GROUPING_ID, - maxGroupingLevels: MAX_GROUPING_LEVELS, - title: groupingTitle, - onGroupChange: () => { - setActivePageIndex(0); - }, + getDefaultQuery, + unit: FINDINGS_UNIT, }); - const selectedGroup = grouping.selectedGroups[0]; - const isNoneSelected = isNoneGroup(grouping.selectedGroups); - - const uniqueValue = useMemo(() => `${selectedGroup}-${uuid.v4()}`, [selectedGroup]); - const groupingQuery = getGroupingQuery({ additionalFilters: [query], groupByField: selectedGroup, @@ -215,11 +131,25 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { enabled: !isNoneSelected, }); - const aggs = useMemo( + const groupData = useMemo( () => parseGroupingQuery(selectedGroup, uniqueValue, data), [data, selectedGroup, uniqueValue] ); + const renderChildComponent = useCallback( + (groupFilters: Filter[]) => { + return ( + + ); + }, + [dataView] + ); + if (isNoneSelected) { return ( <> @@ -233,41 +163,19 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { } return ( -
+
- {grouping.getGrouping({ - activePage: activePageIndex, - data: aggs, - groupingLevel: 0, - inspectButton: undefined, - isLoading: isFetching, - itemsPerPage: pageSize, - onChangeGroupsItemsPerPage: (size) => { - setPageSize(size); - }, - onChangeGroupsPage: (index) => { - setActivePageIndex(index); - }, - renderChildComponent: (groupFilter) => { - return ( - - ); - }, - onGroupClose: () => {}, - selectedGroup, - takeActionItems: () => [], - })} +
); }; @@ -307,8 +215,9 @@ export const LatestFindingsTable = ({ return ( - {error && } - {!error && ( + {error ? ( + + ) : ( <> { + const onDistributionBarClick = (evaluation: Evaluation) => { setUrlQuery({ filters: getFilters({ filters, @@ -384,21 +293,22 @@ export const LatestFindingsContainerTable = ({ }); }; + const canShowDistributionBar = showDistributionBar && total > 0; + return ( - {error && ( + {error ? ( <> - )} - {!error && ( + ) : ( <> - {showDistributionBar && total > 0 && ( + {canShowDistributionBar && ( <> From dcde804cac1e136336553cb8638ef2f87a01751d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sun, 12 Nov 2023 23:48:22 -0800 Subject: [PATCH 16/63] splitting code into hooks and components --- .../latest_findings_container.tsx | 338 ++---------------- .../latest_findings/latest_findings_table.tsx | 148 ++++++++ .../use_latest_findings_grouping.tsx | 71 ++++ .../use_latest_findings_table.tsx | 86 +++++ 4 files changed, 340 insertions(+), 303 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index b79c5e1441e5d..f8ad0e140aaea 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -4,142 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { DataTableRecord } from '@kbn/discover-utils/types'; +import React, { useCallback } from 'react'; import { Filter } from '@kbn/es-query'; -import { getGroupingQuery } from '@kbn/securitysolution-grouping'; -import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; -import { - CloudSecurityGrouping, - useCloudSecurityGrouping, -} from '../../../components/cloud_security_grouping'; -import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; -import { TimestampTableCell } from '../../../components/timestamp_table_cell'; -import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; -import type { Evaluation } from '../../../../common/types'; +import { CloudSecurityGrouping } from '../../../components/cloud_security_grouping'; import type { FindingsBaseProps } from '../../../common/types'; import { FindingsSearchBar } from '../layout/findings_search_bar'; -import * as TEST_SUBJECTS from '../test_subjects'; -import { useLatestFindings } from './use_latest_findings'; -import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; -import { getFilters } from '../utils/utils'; -import { ErrorCallout } from '../layout/error_callout'; -import { LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY } from '../../../common/constants'; -import { CspFinding } from '../../../../common/schemas/csp_finding'; -import { useCloudPostureTable } from '../../../common/hooks/use_cloud_posture_table'; -import { CloudSecurityDataTable } from '../../../components/cloud_security_data_table'; -import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; -import { useGroupedFindings } from './use_grouped_findings'; -import { - FINDINGS_UNIT, - groupingTitle, - defaultGroupingOptions, - getDefaultQuery, - DEFAULT_TABLE_HEIGHT, - defaultColumns, -} from './constants'; - -/** - * Type Guard for checking if the given source is a CspFinding - */ -const isCspFinding = (source: Record | undefined): source is CspFinding => { - return source?.result?.evaluation !== undefined; -}; - -/** - * This Wrapper component renders the children if the given row is a CspFinding - * it uses React's Render Props pattern - */ -const CspFindingRenderer = ({ - row, - children, -}: { - row: DataTableRecord; - children: ({ finding }: { finding: CspFinding }) => JSX.Element; -}) => { - const source = row.raw._source; - const finding = isCspFinding(source) && (source as CspFinding); - if (!finding) return <>; - return children({ finding }); -}; - -/** - * Flyout component for the latest findings table - */ -const flyoutComponent = (row: DataTableRecord, onCloseFlyout: () => void): JSX.Element => { - return ( - - {({ finding }) => } - - ); -}; - -const columnsLocalStorageKey = 'cloudPosture:latestFindings:columns'; - -const title = i18n.translate('xpack.csp.findings.latestFindings.tableRowTypeLabel', { - defaultMessage: 'Findings', -}); - -const customCellRenderer = (rows: DataTableRecord[]) => ({ - 'result.evaluation': ({ rowIndex }: EuiDataGridCellValueElementProps) => ( - - {({ finding }) => } - - ), - '@timestamp': ({ rowIndex }: EuiDataGridCellValueElementProps) => ( - - {({ finding }) => } - - ), -}); +import { DEFAULT_TABLE_HEIGHT } from './constants'; +import { useLatestFindingsGrouping } from './use_latest_findings_grouping'; +import { LatestFindingsTable } from './latest_findings_table'; export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { - const { - activePageIndex, - grouping, - pageSize, - query, - selectedGroup, - onChangeGroupsItemsPerPage, - onChangeGroupsPage, - setUrlQuery, - uniqueValue, - isNoneSelected, - } = useCloudSecurityGrouping({ - dataView, - groupingTitle, - defaultGroupingOptions, - getDefaultQuery, - unit: FINDINGS_UNIT, - }); - - const groupingQuery = getGroupingQuery({ - additionalFilters: [query], - groupByField: selectedGroup, - uniqueValue, - from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, - to: 'now', - pageNumber: activePageIndex * pageSize, - size: pageSize, - sort: [{ _key: { order: 'asc' } }], - }); - - const { data, isFetching } = useGroupedFindings({ - query: groupingQuery, - enabled: !isNoneSelected, - }); - - const groupData = useMemo( - () => parseGroupingQuery(selectedGroup, uniqueValue, data), - [data, selectedGroup, uniqueValue] - ); - const renderChildComponent = useCallback( (groupFilters: Filter[]) => { return ( - { [dataView] ); - if (isNoneSelected) { + const { + canRenderGrouping, + groupData, + grouping, + isFetching, + activePageIndex, + pageSize, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + } = useLatestFindingsGrouping({ dataView }); + + if (canRenderGrouping) { return ( - <> +
- - +
); } return ( -
+ <> - -
- ); -}; - -export const LatestFindingsTable = ({ - dataView, - filter, -}: FindingsBaseProps & { - filter: Filter[]; -}) => { - const cloudPostureTable = useCloudPostureTable({ - dataView, - paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, - columnsLocalStorageKey, - defaultQuery: getDefaultQuery, - additionalFilters: filter, - }); - - const { query, sort, queryError, getRowsFromPages } = cloudPostureTable; - - const { - data, - error: fetchError, - isFetching, - fetchNextPage, - } = useLatestFindings({ - query, - sort, - enabled: !queryError, - }); - - const rows = useMemo(() => getRowsFromPages(data?.pages), [data?.pages, getRowsFromPages]); - - const error = fetchError || queryError; - const total = data?.pages[0].total || 0; - - return ( - - - {error ? ( - - ) : ( - <> - - - - )} - - ); -}; - -export const LatestFindingsContainerTable = ({ - dataView, - groupSelectorComponent, - height, - showDistributionBar = true, - additionalFilters, -}: FindingsBaseProps & { - groupSelectorComponent?: JSX.Element; - height?: number; - showDistributionBar?: boolean; - additionalFilters?: Filter[]; -}) => { - const cloudPostureTable = useCloudPostureTable({ - dataView, - paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, - columnsLocalStorageKey, - defaultQuery: getDefaultQuery, - additionalFilters, - }); - - const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureTable; - - const { - data, - error: fetchError, - isFetching, - fetchNextPage, - } = useLatestFindings({ - query, - sort, - enabled: !queryError, - }); - - const rows = useMemo(() => getRowsFromPages(data?.pages), [data?.pages, getRowsFromPages]); - - const error = fetchError || queryError; - - const passed = data?.pages[0].count.passed || 0; - const failed = data?.pages[0].count.failed || 0; - const total = data?.pages[0].total || 0; - - const onDistributionBarClick = (evaluation: Evaluation) => { - setUrlQuery({ - filters: getFilters({ - filters, - dataView, - field: 'result.evaluation', - value: evaluation, - negate: false, - }), - }); - }; - - const canShowDistributionBar = showDistributionBar && total > 0; - - return ( - - {error ? ( - <> - - - - ) : ( - <> - {canShowDistributionBar && ( - <> - - - - - )} - - - )} - + + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx new file mode 100644 index 0000000000000..16b9aa687b98e --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx @@ -0,0 +1,148 @@ +/* + * 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 { Filter } from '@kbn/es-query'; +import { DataTableRecord } from '@kbn/discover-utils/types'; +import { i18n } from '@kbn/i18n'; +import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { FindingsBaseProps } from '../../../common/types'; +import * as TEST_SUBJECTS from '../test_subjects'; +import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; +import { ErrorCallout } from '../layout/error_callout'; +import { CloudSecurityDataTable } from '../../../components/cloud_security_data_table'; +import { getDefaultQuery, defaultColumns } from './constants'; +import { useLatestFindingsTable } from './use_latest_findings_table'; +import { TimestampTableCell } from '../../../components/timestamp_table_cell'; +import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; +import { CspFinding } from '../../../../common/schemas/csp_finding'; +import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; + +type LatestFindingsTableProps = FindingsBaseProps & { + groupSelectorComponent?: JSX.Element; + height?: number; + showDistributionBar?: boolean; + additionalFilters?: Filter[]; +}; + +/** + * Type Guard for checking if the given source is a CspFinding + */ +const isCspFinding = (source: Record | undefined): source is CspFinding => { + return source?.result?.evaluation !== undefined; +}; + +/** + * This Wrapper component renders the children if the given row is a CspFinding + * it uses React's Render Props pattern + */ +const CspFindingRenderer = ({ + row, + children, +}: { + row: DataTableRecord; + children: ({ finding }: { finding: CspFinding }) => JSX.Element; +}) => { + const source = row.raw._source; + const finding = isCspFinding(source) && (source as CspFinding); + if (!finding) return <>; + return children({ finding }); +}; + +/** + * Flyout component for the latest findings table + */ +const flyoutComponent = (row: DataTableRecord, onCloseFlyout: () => void): JSX.Element => { + return ( + + {({ finding }) => } + + ); +}; + +const title = i18n.translate('xpack.csp.findings.latestFindings.tableRowTypeLabel', { + defaultMessage: 'Findings', +}); + +const customCellRenderer = (rows: DataTableRecord[]) => ({ + 'result.evaluation': ({ rowIndex }: EuiDataGridCellValueElementProps) => ( + + {({ finding }) => } + + ), + '@timestamp': ({ rowIndex }: EuiDataGridCellValueElementProps) => ( + + {({ finding }) => } + + ), +}); + +export const LatestFindingsTable = ({ + dataView, + groupSelectorComponent, + height, + showDistributionBar = true, + additionalFilters, +}: LatestFindingsTableProps) => { + const { + cloudPostureTable, + rows, + error, + isFetching, + fetchNextPage, + passed, + failed, + total, + canShowDistributionBar, + onDistributionBarClick, + } = useLatestFindingsTable({ + dataView, + getDefaultQuery, + additionalFilters, + showDistributionBar, + }); + + return ( + + {error ? ( + <> + + + + ) : ( + <> + {canShowDistributionBar && ( + <> + + + + + )} + + + )} + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx new file mode 100644 index 0000000000000..711e90c1e8422 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -0,0 +1,71 @@ +/* + * 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 { getGroupingQuery } from '@kbn/securitysolution-grouping'; +import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; +import { useMemo } from 'react'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; +import { useGroupedFindings } from './use_grouped_findings'; +import { FINDINGS_UNIT, groupingTitle, defaultGroupingOptions, getDefaultQuery } from './constants'; +import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; + +export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) => { + const { + activePageIndex, + grouping, + pageSize, + query, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + uniqueValue, + isNoneSelected, + } = useCloudSecurityGrouping({ + dataView, + groupingTitle, + defaultGroupingOptions, + getDefaultQuery, + unit: FINDINGS_UNIT, + }); + + const groupingQuery = getGroupingQuery({ + additionalFilters: [query], + groupByField: selectedGroup, + uniqueValue, + from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + to: 'now', + pageNumber: activePageIndex * pageSize, + size: pageSize, + sort: [{ _key: { order: 'asc' } }], + }); + + const { data, isFetching } = useGroupedFindings({ + query: groupingQuery, + enabled: !isNoneSelected, + }); + + const groupData = useMemo( + () => parseGroupingQuery(selectedGroup, uniqueValue, data), + [data, selectedGroup, uniqueValue] + ); + + const canRenderGrouping = !isNoneSelected; + + return { + canRenderGrouping, + groupData, + grouping, + isFetching, + activePageIndex, + pageSize, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx new file mode 100644 index 0000000000000..92c63e2935515 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx @@ -0,0 +1,86 @@ +/* + * 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 { DataView } from '@kbn/data-views-plugin/common'; +import { Filter } from '@kbn/es-query'; +import { useMemo } from 'react'; +import { FindingsBaseURLQuery } from '../../../common/types'; +import { Evaluation } from '../../../../common/types'; +import { LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY } from '../../../common/constants'; +import { useCloudPostureTable } from '../../../common/hooks/use_cloud_posture_table'; +import { getFilters } from '../utils/get_filters'; +import { useLatestFindings } from './use_latest_findings'; + +const columnsLocalStorageKey = 'cloudPosture:latestFindings:columns'; + +export const useLatestFindingsTable = ({ + dataView, + getDefaultQuery, + additionalFilters, + showDistributionBar, +}: { + dataView: DataView; + getDefaultQuery: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; + additionalFilters?: Filter[]; + showDistributionBar?: boolean; +}) => { + const cloudPostureTable = useCloudPostureTable({ + dataView, + paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, + columnsLocalStorageKey, + defaultQuery: getDefaultQuery, + additionalFilters, + }); + + const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureTable; + + const { + data, + error: fetchError, + isFetching, + fetchNextPage, + } = useLatestFindings({ + query, + sort, + enabled: !queryError, + }); + + const rows = useMemo(() => getRowsFromPages(data?.pages), [data?.pages, getRowsFromPages]); + + const error = fetchError || queryError; + + const passed = data?.pages[0].count.passed || 0; + const failed = data?.pages[0].count.failed || 0; + const total = data?.pages[0].total || 0; + + const onDistributionBarClick = (evaluation: Evaluation) => { + setUrlQuery({ + filters: getFilters({ + filters, + dataView, + field: 'result.evaluation', + value: evaluation, + negate: false, + }), + }); + }; + + const canShowDistributionBar = showDistributionBar && total > 0; + + return { + cloudPostureTable, + rows, + error, + isFetching, + fetchNextPage, + passed, + failed, + total, + canShowDistributionBar, + onDistributionBarClick, + }; +}; From 97c19a375a7125aa012c4dbef4f69a0fd72c4c90 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 13 Nov 2023 01:59:09 -0800 Subject: [PATCH 17/63] addressing pr comments, removing anys --- .../use_cloud_posture_table.ts | 32 ++++++++----------- .../hooks/use_cloud_posture_table/utils.ts | 8 ++--- .../public/common/types.ts | 2 +- .../latest_findings_container.tsx | 12 ++++--- .../latest_findings/latest_findings_table.tsx | 6 ++-- .../use_latest_findings_grouping.tsx | 5 ++- .../use_latest_findings_table.tsx | 6 ++-- 7 files changed, 35 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts index 7172adbf26947..59ac5a741379f 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts @@ -13,28 +13,25 @@ import { useUrlQuery } from '../use_url_query'; import { usePageSize } from '../use_page_size'; import { getDefaultQuery, useBaseEsQuery, usePersistedQuery } from './utils'; import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants'; +import { FindingsBaseURLQuery } from '../../types'; + +type URLQuery = FindingsBaseURLQuery & Record; export interface CloudPostureTableResult { - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - setUrlQuery: (query: any) => void; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - sort: any; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - filters: any[]; + setUrlQuery: (query: Record) => void; + sort: string[][]; + filters: Filter[]; query: { bool: BoolQuery }; queryError?: Error; pageIndex: number; - // TODO: remove any, urlQuery is an object with query fields but we also add custom fields to it, need to assert usages - urlQuery: any; + urlQuery: URLQuery; setTableOptions: (options: CriteriaWithPagination) => void; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - handleUpdateQuery: (query: any) => void; + handleUpdateQuery: (query: URLQuery) => void; pageSize: number; setPageSize: Dispatch>; onChangeItemsPerPage: (newPageSize: number) => void; onChangePage: (newPageIndex: number) => void; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - onSort: (sort: any) => void; + onSort: (sort: string[]) => void; onResetFilters: () => void; columnsLocalStorageKey: string; getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[]; @@ -48,17 +45,16 @@ export const useCloudPostureTable = ({ dataView, paginationLocalStorageKey, columnsLocalStorageKey, - additionalFilters, + nonPersistedFilters, }: { - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - defaultQuery?: (params: any) => any; + defaultQuery?: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; dataView: DataView; paginationLocalStorageKey: string; columnsLocalStorageKey?: string; - additionalFilters?: Filter[]; + nonPersistedFilters?: Filter[]; }): CloudPostureTableResult => { const getPersistedDefaultQuery = usePersistedQuery(defaultQuery); - const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); const { pageSize, setPageSize } = usePageSize(paginationLocalStorageKey); const onChangeItemsPerPage = useCallback( @@ -119,7 +115,7 @@ export const useCloudPostureTable = ({ dataView, filters: urlQuery.filters, query: urlQuery.query, - ...(additionalFilters ? { additionalFilters } : {}), + ...(nonPersistedFilters ? { nonPersistedFilters } : {}), }); const handleUpdateQuery = useCallback( diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts index 292a0007f519a..e86d2a77589b0 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts @@ -54,9 +54,9 @@ export const getPaginationQuery = ({ export const useBaseEsQuery = ({ dataView, - filters, + filters = [], query, - additionalFilters, + nonPersistedFilters, }: FindingsBaseURLQuery & FindingsBaseProps) => { const { notifications: { toasts }, @@ -71,11 +71,11 @@ export const useBaseEsQuery = ({ () => getBaseQuery({ dataView, - filters: filters.concat(additionalFilters ?? []).flat(), + filters: filters.concat(nonPersistedFilters ?? []).flat(), query, config, }), - [dataView, filters, additionalFilters, query, config] + [dataView, filters, nonPersistedFilters, query, config] ); /** diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index d35127a410a09..a4c26643293fd 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -17,7 +17,7 @@ export interface FindingsBaseURLQuery { /** * Filters that are part of the query but not persisted in the URL or in the Filter Manager */ - additionalFilters?: Filter[]; + nonPersistedFilters?: Filter[]; } export interface FindingsBaseProps { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index f8ad0e140aaea..613594c66e939 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback } from 'react'; import { Filter } from '@kbn/es-query'; +import { defaultLoadingRenderer } from '../../../components/cloud_posture_page'; import { CloudSecurityGrouping } from '../../../components/cloud_security_grouping'; import type { FindingsBaseProps } from '../../../common/types'; import { FindingsSearchBar } from '../layout/findings_search_bar'; @@ -19,7 +20,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { return ( @@ -29,7 +30,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { ); const { - canRenderGrouping, + isGroupSelect, groupData, grouping, isFetching, @@ -39,10 +40,13 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { onChangeGroupsItemsPerPage, onChangeGroupsPage, setUrlQuery, + isGroupLoading, } = useLatestFindingsGrouping({ dataView }); - if (canRenderGrouping) { - return ( + if (isGroupSelect) { + return isGroupLoading ? ( + defaultLoadingRenderer() + ) : (
{ const { cloudPostureTable, @@ -102,7 +102,7 @@ export const LatestFindingsTable = ({ } = useLatestFindingsTable({ dataView, getDefaultQuery, - additionalFilters, + nonPersistedFilters, showDistributionBar, }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 711e90c1e8422..0675f05804884 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -54,10 +54,7 @@ export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) [data, selectedGroup, uniqueValue] ); - const canRenderGrouping = !isNoneSelected; - return { - canRenderGrouping, groupData, grouping, isFetching, @@ -67,5 +64,7 @@ export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) onChangeGroupsItemsPerPage, onChangeGroupsPage, setUrlQuery, + isGroupSelect: !isNoneSelected, + isGroupLoading: !data, }; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx index 92c63e2935515..5568a484a18ea 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx @@ -20,12 +20,12 @@ const columnsLocalStorageKey = 'cloudPosture:latestFindings:columns'; export const useLatestFindingsTable = ({ dataView, getDefaultQuery, - additionalFilters, + nonPersistedFilters, showDistributionBar, }: { dataView: DataView; getDefaultQuery: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; - additionalFilters?: Filter[]; + nonPersistedFilters?: Filter[]; showDistributionBar?: boolean; }) => { const cloudPostureTable = useCloudPostureTable({ @@ -33,7 +33,7 @@ export const useLatestFindingsTable = ({ paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, columnsLocalStorageKey, defaultQuery: getDefaultQuery, - additionalFilters, + nonPersistedFilters, }); const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureTable; From be967d22a3b663e2aeec57dd784a926e76f3cf8c Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 00:04:00 -0800 Subject: [PATCH 18/63] findings grouping FTR tests --- .../cloud_security_grouping.tsx | 1 + .../page_objects/findings_page.ts | 42 +++ .../pages/findings.ts | 24 -- .../pages/findings_grouping.ts | 310 ++++++++++++++++++ .../pages/index.ts | 1 + 5 files changed, 354 insertions(+), 24 deletions(-) create mode 100644 x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx index 00b96e77d6402..067046ce5457a 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx @@ -35,6 +35,7 @@ export const CloudSecurityGrouping = ({ }: CloudSecurityGroupingProps) => { return (
({ + async getElement() { + return await testSubjects.find(testSubj); + }, + async setValue(value: string) { + const contextMenu = await testSubjects.find('groupByContextMenu'); + const menuItems = await contextMenu.findAllByCssSelector('button.euiContextMenuItem'); + const menuItemsOptions = await Promise.all(menuItems.map((item) => item.getVisibleText())); + const menuItemValueIndex = menuItemsOptions.findIndex((item) => item === value); + await menuItems[menuItemValueIndex].click(); + return await testSubjects.missingOrFail('is-loading-grouping-table', { timeout: 5000 }); + }, + async openDropDown() { + const element = await this.getElement(); + await element.click(); + }, + }); + + const findingsGrouping = async (testSubj = 'cloudSecurityGrouping') => ({ + async getElement() { + return await testSubjects.find(testSubj); + }, + async getGroupCount() { + const element = await this.getElement(); + const groupCount = await element.findByTestSubject('group-count'); + return await groupCount.getVisibleText(); + }, + async getUnitCount() { + const element = await this.getElement(); + const unitCount = await element.findByTestSubject('unit-count'); + return await unitCount.getVisibleText(); + }, + async getRowAtIndex(rowIndex: number) { + const element = await this.getElement(); + const row = await element.findAllByTestSubject('grouping-accordion'); + return await row[rowIndex]; + }, + }); + return { navigateToLatestFindingsPage, navigateToVulnerabilities, @@ -426,5 +465,8 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider misconfigurationsFlyout, toastMessage, detectionRuleApi, + groupSelector, + findingsGrouping, + createDataTableObject, }; } diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index 69c1fd6949f51..24ce9897d2169 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -13,7 +13,6 @@ import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const filterBar = getService('filterBar'); - const comboBox = getService('comboBox'); const retry = getService('retry'); const pageObjects = getPageObjects(['common', 'findings', 'header']); const chance = new Chance(); @@ -94,24 +93,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const ruleName1 = data[0].rule.name; const ruleName2 = data[1].rule.name; - const resourceId1 = data[0].resource.id; - const ruleSection1 = data[0].rule.section; - - const benchMarkName = data[0].rule.benchmark.name; - describe('Findings Page', function () { this.tags(['cloud_security_posture_findings']); let findings: typeof pageObjects.findings; let latestFindingsTable: typeof findings.latestFindingsTable; - let findingsByResourceTable: typeof findings.findingsByResourceTable; - let resourceFindingsTable: typeof findings.resourceFindingsTable; let distributionBar: typeof findings.distributionBar; before(async () => { findings = pageObjects.findings; latestFindingsTable = findings.latestFindingsTable; - findingsByResourceTable = findings.findingsByResourceTable; - resourceFindingsTable = findings.resourceFindingsTable; distributionBar = findings.distributionBar; // Before we start any test we must wait for cloud_security_posture plugin to complete its initialization @@ -219,19 +209,5 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); }); - - describe('GroupBy', () => { - it('groups findings by resource', async () => { - await comboBox.set('findings_group_by_selector', 'Resource'); - expect( - await findingsByResourceTable.hasColumnValue('Applicable Benchmark', benchMarkName) - ).to.be(true); - }); - - it('navigates to resource findings page from resource id link', async () => { - await findingsByResourceTable.clickResourceIdLink(resourceId1, ruleSection1); - expect(await resourceFindingsTable.hasColumnValue('Rule Name', ruleName1)).to.be(true); - }); - }); }); } diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts new file mode 100644 index 0000000000000..cbd13ae54bec4 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts @@ -0,0 +1,310 @@ +/* + * 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 expect from '@kbn/expect'; +import Chance from 'chance'; +import { asyncForEach } from '@kbn/std'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); + const retry = getService('retry'); + const pageObjects = getPageObjects(['common', 'findings', 'header']); + const chance = new Chance(); + + // We need to use a dataset for the tests to run + // We intentionally make some fields start with a capital letter to test that the query bar is case-insensitive/case-sensitive + const data = [ + { + '@timestamp': '1695819664234', + resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + orchestrator: { + cluster: { + id: '1', + name: 'Cluster 1', + }, + }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'Upper case cluster id', + }, + { + '@timestamp': '1695819673242', + resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + cloud: { + account: { + id: '1', + name: 'Account 1', + }, + }, + rule: { + name: 'lower case rule name', + section: 'Another upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'Another Upper case cluster id', + }, + { + '@timestamp': '1695819676242', + resource: { id: chance.guid(), name: `process`, sub_type: 'another lower case type' }, + result: { evaluation: 'passed' }, + cloud: { + account: { + id: '1', + name: 'Account 1', + }, + }, + rule: { + name: 'Another upper case rule name', + section: 'lower case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'lower case cluster id', + }, + { + '@timestamp': '1695819680202', + resource: { id: chance.guid(), name: `process`, sub_type: 'Upper case type again' }, + result: { evaluation: 'failed' }, + cloud: { + account: { + id: '2', + name: 'Account 2', + }, + }, + rule: { + name: 'some lower case rule name', + section: 'another lower case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'another lower case cluster id', + }, + ]; + + const ruleName1 = data[0].rule.name; + + describe('Findings Page - Grouping', function () { + this.tags(['cloud_security_posture_findings_grouping']); + let findings: typeof pageObjects.findings; + + before(async () => { + findings = pageObjects.findings; + + // Before we start any test we must wait for cloud_security_posture plugin to complete its initialization + await findings.waitForPluginInitialized(); + + // Prepare mocked findings + await findings.index.remove(); + await findings.index.add(data); + + await findings.navigateToLatestFindingsPage(); + + await retry.waitFor( + 'Findings table to be loaded', + async () => (await findings.latestFindingsTable.getRowsCount()) === data.length + ); + pageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await findings.index.remove(); + }); + + describe('Default Grouping', async () => { + it('groups findings by resource and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('Resource'); + + const grouping = await findings.findingsGrouping(); + + const resourceOrder = ['Pod', 'kubelet', 'process']; + + await asyncForEach(resourceOrder, async (resourceName, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.be(resourceName); + }); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('3 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('4 findings'); + }); + it('groups findings by rule name and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('Rule name'); + + const grouping = await findings.findingsGrouping(); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('4 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('4 findings'); + + const ruleNameOrder = [ + 'Another upper case rule name', + 'Upper case rule name', + 'lower case rule name', + 'some lower case rule name', + ]; + + await asyncForEach(ruleNameOrder, async (resourceName, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.be(resourceName); + }); + }); + it('groups findings by cloud account and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.setValue('Cloud account'); + + const grouping = await findings.findingsGrouping(); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('3 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('4 findings'); + + const cloudNameOrder = ['Account 1', 'Account 2', '—']; + + await asyncForEach(cloudNameOrder, async (resourceName, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.be(resourceName); + }); + }); + it('groups findings by Kubernetes cluster and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.setValue('Kubernetes cluster'); + + const grouping = await findings.findingsGrouping(); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('2 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('4 findings'); + + const cloudNameOrder = ['Cluster 1', '—']; + + await asyncForEach(cloudNameOrder, async (resourceName, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.be(resourceName); + }); + }); + }); + describe('SearchBar', () => { + it('add filter', async () => { + // Filter bar uses the field's customLabel in the DataView + const groupSelector = await findings.groupSelector(); + await groupSelector.setValue('Resource'); + + await filterBar.addFilter({ field: 'Rule Name', operation: 'is', value: ruleName1 }); + expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); + + const grouping = await findings.findingsGrouping(); + + const resourceOrder = ['kubelet']; + + await asyncForEach(resourceOrder, async (resourceName, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.be(resourceName); + }); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('1 group'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('1 finding'); + }); + + it('remove filter', async () => { + await filterBar.removeFilter('rule.name'); + + expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(false); + + const grouping = await findings.findingsGrouping(); + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('3 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('4 findings'); + }); + + it('set search query', async () => { + await queryBar.setQuery(ruleName1); + await queryBar.submitQuery(); + + const grouping = await findings.findingsGrouping(); + + const resourceOrder = ['kubelet']; + + await asyncForEach(resourceOrder, async (resourceName, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.be(resourceName); + }); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('1 group'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('1 finding'); + + await queryBar.setQuery(''); + await queryBar.submitQuery(); + + expect(await grouping.getGroupCount()).to.be('3 groups'); + expect(await grouping.getUnitCount()).to.be('4 findings'); + }); + }); + + describe('Group table', async () => { + it('shows findings table when expanding', async () => { + const grouping = await findings.findingsGrouping(); + const firstRow = await grouping.getRowAtIndex(0); + await (await firstRow.findByCssSelector('button')).click(); + const latestFindingsTable = findings.createDataTableObject('latest_findings_table'); + expect(await latestFindingsTable.getRowsCount()).to.be(1); + expect(await latestFindingsTable.hasColumnValue('rule.name', 'lower case rule name')).to.be( + true + ); + }); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 9d4e17ec0c88c..035f2b835492a 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { loadTestFile(require.resolve('./findings_onboarding')); loadTestFile(require.resolve('./findings')); + loadTestFile(require.resolve('./findings_grouping')); loadTestFile(require.resolve('./findings_alerts')); loadTestFile(require.resolve('./compliance_dashboard')); loadTestFile(require.resolve('./vulnerability_dashboard')); From 199f938fce6e9a8c42eb7cbc57bca3bb27af5d09 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 00:14:48 -0800 Subject: [PATCH 19/63] reverting change --- x-pack/plugins/cloud_security_posture/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 5987881aed444..1cc356cbfd5e3 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -45,7 +45,7 @@ export const LATEST_FINDINGS_INDEX_TEMPLATE_NAME = 'logs-cloud_security_posture. export const LATEST_FINDINGS_INDEX_PATTERN = 'logs-cloud_security_posture.findings_latest-*'; export const LATEST_FINDINGS_INDEX_DEFAULT_NS = 'logs-cloud_security_posture.findings_latest-default'; -export const LATEST_FINDINGS_RETENTION_POLICY = '26y'; +export const LATEST_FINDINGS_RETENTION_POLICY = '26h'; export const BENCHMARK_SCORE_INDEX_TEMPLATE_NAME = 'logs-cloud_security_posture.scores'; export const BENCHMARK_SCORE_INDEX_PATTERN = 'logs-cloud_security_posture.scores-*'; From f51a462f2ccbc315814b59930900f90e5d348725 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 07:50:13 -0800 Subject: [PATCH 20/63] adding comments and time filtering --- .../use_cloud_security_grouping.ts | 8 ++++++- .../latest_findings/use_latest_findings.ts | 23 +++++++++++++++++-- .../use_latest_findings_grouping.tsx | 4 ++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index 1235291e62515..ae8aa55d39cbc 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -19,6 +19,9 @@ const DEFAULT_PAGE_SIZE = 10; const GROUPING_ID = 'cspLatestFindings'; const MAX_GROUPING_LEVELS = 1; +/* + Utility hook to handle the grouping logic of the cloud security components +*/ export const useCloudSecurityGrouping = ({ dataView, groupingTitle, @@ -66,10 +69,13 @@ export const useCloudSecurityGrouping = ({ }); const selectedGroup = grouping.selectedGroups[0]; - const isNoneSelected = isNoneGroup(grouping.selectedGroups); + // This is recommended by the grouping component to cover an edge case + // where the selectedGroup has multiple values const uniqueValue = useMemo(() => `${selectedGroup}-${uuid.v4()}`, [selectedGroup]); + const isNoneSelected = isNoneGroup(grouping.selectedGroups); + const onChangeGroupsItemsPerPage = (size: number) => { setActivePageIndex(0); setPageSize(size); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts index 9ce0292175839..0c0aee860d344 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts @@ -16,7 +16,10 @@ import { CspFinding } from '../../../../common/schemas/csp_finding'; import { useKibana } from '../../../common/hooks/use_kibana'; import type { FindingsBaseEsQuery } from '../../../common/types'; import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; -import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; +import { + CSP_LATEST_FINDINGS_DATA_VIEW, + LATEST_FINDINGS_RETENTION_POLICY, +} from '../../../../common/constants'; import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; import { showErrorToast } from '../../../common/utils/show_error_toast'; @@ -41,11 +44,27 @@ interface FindingsAggs { export const getFindingsQuery = ({ query, sort }: UseFindingsOptions, pageParam: any) => ({ index: CSP_LATEST_FINDINGS_DATA_VIEW, - query, sort: getMultiFieldsSort(sort), size: MAX_FINDINGS_TO_LOAD, aggs: getFindingsCountAggQuery(), ignore_unavailable: false, + query: { + ...query, + bool: { + ...query?.bool, + filter: [ + ...(query?.bool?.filter ?? []), + { + range: { + '@timestamp': { + gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + lte: 'now', + }, + }, + }, + ], + }, + }, ...(pageParam ? { search_after: pageParam } : {}), }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 0675f05804884..137716967460f 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -13,6 +13,10 @@ import { useGroupedFindings } from './use_grouped_findings'; import { FINDINGS_UNIT, groupingTitle, defaultGroupingOptions, getDefaultQuery } from './constants'; import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; +/** + * Utility hook to get the latest findings grouping data + * for the findings page + */ export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) => { const { activePageIndex, From 1af2377217e0e28fe101352ce7ce340a5d1c75ca Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 09:06:52 -0800 Subject: [PATCH 21/63] revert use posture table --- .../use_cloud_posture_table.ts | 37 +++++++++---------- .../hooks/use_cloud_posture_table/utils.ts | 22 +++++------ 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts index 59ac5a741379f..0becb56e6ec22 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts @@ -6,32 +6,35 @@ */ import { Dispatch, SetStateAction, useCallback } from 'react'; import { type DataView } from '@kbn/data-views-plugin/common'; -import { BoolQuery, Filter } from '@kbn/es-query'; +import { BoolQuery } from '@kbn/es-query'; import { CriteriaWithPagination } from '@elastic/eui'; import { DataTableRecord } from '@kbn/discover-utils/types'; import { useUrlQuery } from '../use_url_query'; import { usePageSize } from '../use_page_size'; import { getDefaultQuery, useBaseEsQuery, usePersistedQuery } from './utils'; import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants'; -import { FindingsBaseURLQuery } from '../../types'; - -type URLQuery = FindingsBaseURLQuery & Record; export interface CloudPostureTableResult { - setUrlQuery: (query: Record) => void; - sort: string[][]; - filters: Filter[]; - query: { bool: BoolQuery }; + // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable + setUrlQuery: (query: any) => void; + // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable + sort: any; + // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable + filters: any[]; + query?: { bool: BoolQuery }; queryError?: Error; pageIndex: number; - urlQuery: URLQuery; + // TODO: remove any, urlQuery is an object with query fields but we also add custom fields to it, need to assert usages + urlQuery: any; setTableOptions: (options: CriteriaWithPagination) => void; - handleUpdateQuery: (query: URLQuery) => void; + // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable + handleUpdateQuery: (query: any) => void; pageSize: number; setPageSize: Dispatch>; onChangeItemsPerPage: (newPageSize: number) => void; onChangePage: (newPageIndex: number) => void; - onSort: (sort: string[]) => void; + // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable + onSort: (sort: any) => void; onResetFilters: () => void; columnsLocalStorageKey: string; getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[]; @@ -45,16 +48,15 @@ export const useCloudPostureTable = ({ dataView, paginationLocalStorageKey, columnsLocalStorageKey, - nonPersistedFilters, }: { - defaultQuery?: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; + // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable + defaultQuery?: (params: any) => any; dataView: DataView; paginationLocalStorageKey: string; columnsLocalStorageKey?: string; - nonPersistedFilters?: Filter[]; }): CloudPostureTableResult => { const getPersistedDefaultQuery = usePersistedQuery(defaultQuery); - const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); const { pageSize, setPageSize } = usePageSize(paginationLocalStorageKey); const onChangeItemsPerPage = useCallback( @@ -115,7 +117,6 @@ export const useCloudPostureTable = ({ dataView, filters: urlQuery.filters, query: urlQuery.query, - ...(nonPersistedFilters ? { nonPersistedFilters } : {}), }); const handleUpdateQuery = useCallback( @@ -132,14 +133,12 @@ export const useCloudPostureTable = ({ }) .flat() || []; - const queryError = baseEsQuery instanceof Error ? baseEsQuery : undefined; - return { setUrlQuery, sort: urlQuery.sort, filters: urlQuery.filters, query: baseEsQuery.query, - queryError, + queryError: baseEsQuery.error, pageIndex: urlQuery.pageIndex, urlQuery, setTableOptions, diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts index e86d2a77589b0..2d3f9b1c7605c 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts @@ -28,7 +28,10 @@ const getBaseQuery = ({ query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query }; } catch (error) { - throw new Error(error); + return { + query: undefined, + error: error instanceof Error ? error : new Error('Unknown Error'), + }; } }; @@ -54,9 +57,8 @@ export const getPaginationQuery = ({ export const useBaseEsQuery = ({ dataView, - filters = [], + filters, query, - nonPersistedFilters, }: FindingsBaseURLQuery & FindingsBaseProps) => { const { notifications: { toasts }, @@ -68,14 +70,8 @@ export const useBaseEsQuery = ({ const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards'); const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]); const baseEsQuery = useMemo( - () => - getBaseQuery({ - dataView, - filters: filters.concat(nonPersistedFilters ?? []).flat(), - query, - config, - }), - [dataView, filters, nonPersistedFilters, query, config] + () => getBaseQuery({ dataView, filters, query, config }), + [dataView, filters, query, config] ); /** @@ -87,7 +83,7 @@ export const useBaseEsQuery = ({ }, [filters, filterManager, queryString, query]); const handleMalformedQueryError = () => { - const error = baseEsQuery instanceof Error ? baseEsQuery : undefined; + const error = baseEsQuery.error; if (error) { toasts.addError(error, { title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { @@ -98,7 +94,7 @@ export const useBaseEsQuery = ({ } }; - useEffect(handleMalformedQueryError, [baseEsQuery, toasts]); + useEffect(handleMalformedQueryError, [baseEsQuery.error, toasts]); return baseEsQuery; }; From 1eb9ef26c2bb7fca07520fe60c040dea032a9370 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 09:07:12 -0800 Subject: [PATCH 22/63] add use posture data table --- .../use_cloud_posture_data_table/index.ts | 8 + .../use_cloud_posture_data_table.ts | 158 ++++++++++++++++++ .../use_cloud_posture_data_table/utils.ts | 128 ++++++++++++++ .../use_cloud_security_grouping.ts | 2 +- .../use_latest_findings_table.tsx | 4 +- 5 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts create mode 100644 x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts create mode 100644 x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts new file mode 100644 index 0000000000000..60a917846dc99 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './use_cloud_posture_data_table'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts new file mode 100644 index 0000000000000..b7b928a208c0a --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts @@ -0,0 +1,158 @@ +/* + * 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 { Dispatch, SetStateAction, useCallback } from 'react'; +import { type DataView } from '@kbn/data-views-plugin/common'; +import { BoolQuery, Filter } from '@kbn/es-query'; +import { CriteriaWithPagination } from '@elastic/eui'; +import { DataTableRecord } from '@kbn/discover-utils/types'; +import { useUrlQuery } from '../use_url_query'; +import { usePageSize } from '../use_page_size'; +import { getDefaultQuery, useBaseEsQuery, usePersistedQuery } from './utils'; +import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants'; +import { FindingsBaseURLQuery } from '../../types'; + +type URLQuery = FindingsBaseURLQuery & Record; + +type SortOrder = [string, string]; + +export interface CloudPostureDataTableResult { + setUrlQuery: (query: Record) => void; + sort: SortOrder[]; + filters: Filter[]; + query: { bool: BoolQuery }; + queryError?: Error; + pageIndex: number; + urlQuery: URLQuery; + setTableOptions: (options: CriteriaWithPagination) => void; + handleUpdateQuery: (query: URLQuery) => void; + pageSize: number; + setPageSize: Dispatch>; + onChangeItemsPerPage: (newPageSize: number) => void; + onChangePage: (newPageIndex: number) => void; + onSort: (sort: string[][]) => void; + onResetFilters: () => void; + columnsLocalStorageKey: string; + getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[]; +} + +/* + Hook for managing common table state and methods for the Cloud Posture DataTable +*/ +export const useCloudPostureDataTable = ({ + defaultQuery = getDefaultQuery, + dataView, + paginationLocalStorageKey, + columnsLocalStorageKey, + nonPersistedFilters, +}: { + defaultQuery?: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; + dataView: DataView; + paginationLocalStorageKey: string; + columnsLocalStorageKey?: string; + nonPersistedFilters?: Filter[]; +}): CloudPostureDataTableResult => { + const getPersistedDefaultQuery = usePersistedQuery(defaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + const { pageSize, setPageSize } = usePageSize(paginationLocalStorageKey); + + const onChangeItemsPerPage = useCallback( + (newPageSize) => { + setPageSize(newPageSize); + setUrlQuery({ + pageIndex: 0, + pageSize: newPageSize, + }); + }, + [setPageSize, setUrlQuery] + ); + + const onResetFilters = useCallback(() => { + setUrlQuery({ + pageIndex: 0, + filters: [], + query: { + query: '', + language: 'kuery', + }, + }); + }, [setUrlQuery]); + + const onChangePage = useCallback( + (newPageIndex) => { + setUrlQuery({ + pageIndex: newPageIndex, + }); + }, + [setUrlQuery] + ); + + const onSort = useCallback( + (sort) => { + setUrlQuery({ + sort, + }); + }, + [setUrlQuery] + ); + + const setTableOptions = useCallback( + ({ page, sort }) => { + setPageSize(page.size); + setUrlQuery({ + sort, + pageIndex: page.index, + }); + }, + [setUrlQuery, setPageSize] + ); + + /** + * Page URL query to ES query + */ + const baseEsQuery = useBaseEsQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + ...(nonPersistedFilters ? { nonPersistedFilters } : {}), + }); + + const handleUpdateQuery = useCallback( + (query) => { + setUrlQuery({ ...query, pageIndex: 0 }); + }, + [setUrlQuery] + ); + + const getRowsFromPages = (data: Array<{ page: DataTableRecord[] }> | undefined) => + data + ?.map(({ page }: { page: DataTableRecord[] }) => { + return page; + }) + .flat() || []; + + const queryError = baseEsQuery instanceof Error ? baseEsQuery : undefined; + + return { + setUrlQuery, + sort: urlQuery.sort, + filters: urlQuery.filters, + query: baseEsQuery.query, + queryError, + pageIndex: urlQuery.pageIndex, + urlQuery, + setTableOptions, + handleUpdateQuery, + pageSize, + setPageSize, + onChangeItemsPerPage, + onChangePage, + onSort, + onResetFilters, + columnsLocalStorageKey: columnsLocalStorageKey || LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY, + getRowsFromPages, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts new file mode 100644 index 0000000000000..e86d2a77589b0 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts @@ -0,0 +1,128 @@ +/* + * 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 { useEffect, useCallback, useMemo } from 'react'; +import { buildEsQuery, EsQueryConfig } from '@kbn/es-query'; +import type { EuiBasicTableProps, Pagination } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { type Query } from '@kbn/es-query'; +import { useKibana } from '../use_kibana'; +import type { + FindingsBaseESQueryConfig, + FindingsBaseProps, + FindingsBaseURLQuery, +} from '../../types'; + +const getBaseQuery = ({ + dataView, + query, + filters, + config, +}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { + try { + return { + query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query + }; + } catch (error) { + throw new Error(error); + } +}; + +type TablePagination = NonNullable['pagination']>; + +export const getPaginationTableParams = ( + params: TablePagination & Pick, 'pageIndex' | 'pageSize'>, + pageSizeOptions = [10, 25, 100], + showPerPageOptions = true +): Required => ({ + ...params, + pageSizeOptions, + showPerPageOptions, +}); + +export const getPaginationQuery = ({ + pageIndex, + pageSize, +}: Required>) => ({ + from: pageIndex * pageSize, + size: pageSize, +}); + +export const useBaseEsQuery = ({ + dataView, + filters = [], + query, + nonPersistedFilters, +}: FindingsBaseURLQuery & FindingsBaseProps) => { + const { + notifications: { toasts }, + data: { + query: { filterManager, queryString }, + }, + uiSettings, + } = useKibana().services; + const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards'); + const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]); + const baseEsQuery = useMemo( + () => + getBaseQuery({ + dataView, + filters: filters.concat(nonPersistedFilters ?? []).flat(), + query, + config, + }), + [dataView, filters, nonPersistedFilters, query, config] + ); + + /** + * Sync filters with the URL query + */ + useEffect(() => { + filterManager.setAppFilters(filters); + queryString.setQuery(query); + }, [filters, filterManager, queryString, query]); + + const handleMalformedQueryError = () => { + const error = baseEsQuery instanceof Error ? baseEsQuery : undefined; + if (error) { + toasts.addError(error, { + title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { + defaultMessage: 'Query Error', + }), + toastLifeTimeMs: 1000 * 5, + }); + } + }; + + useEffect(handleMalformedQueryError, [baseEsQuery, toasts]); + + return baseEsQuery; +}; + +export const usePersistedQuery = (getter: ({ filters, query }: FindingsBaseURLQuery) => T) => { + const { + data: { + query: { filterManager, queryString }, + }, + } = useKibana().services; + + return useCallback( + () => + getter({ + filters: filterManager.getAppFilters(), + query: queryString.getQuery() as Query, + }), + [getter, filterManager, queryString] + ); +}; + +export const getDefaultQuery = ({ query, filters }: any): any => ({ + query, + filters, + sort: { field: '@timestamp', direction: 'desc' }, + pageIndex: 0, +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index ae8aa55d39cbc..d2783af516e35 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -12,7 +12,7 @@ import { useUrlQuery } from '../../common/hooks/use_url_query'; import { useBaseEsQuery, usePersistedQuery, -} from '../../common/hooks/use_cloud_posture_table/utils'; +} from '../../common/hooks/use_cloud_posture_data_table/utils'; import { FindingsBaseURLQuery } from '../../common/types'; const DEFAULT_PAGE_SIZE = 10; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx index 5568a484a18ea..2bcc895ddd4b0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx @@ -11,7 +11,7 @@ import { useMemo } from 'react'; import { FindingsBaseURLQuery } from '../../../common/types'; import { Evaluation } from '../../../../common/types'; import { LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY } from '../../../common/constants'; -import { useCloudPostureTable } from '../../../common/hooks/use_cloud_posture_table'; +import { useCloudPostureDataTable } from '../../../common/hooks/use_cloud_posture_data_table'; import { getFilters } from '../utils/get_filters'; import { useLatestFindings } from './use_latest_findings'; @@ -28,7 +28,7 @@ export const useLatestFindingsTable = ({ nonPersistedFilters?: Filter[]; showDistributionBar?: boolean; }) => { - const cloudPostureTable = useCloudPostureTable({ + const cloudPostureTable = useCloudPostureDataTable({ dataView, paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, columnsLocalStorageKey, From a145184ebbbfe86d8549afefbd8ba4961d6c9f6a Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 13:58:23 -0800 Subject: [PATCH 23/63] update FTR tests --- .../page_objects/findings_page.ts | 2 +- .../pages/findings_grouping.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index 9f94edaaec48e..76033f03484e1 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -410,7 +410,7 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider }, }); - const groupSelector = async (testSubj = 'group-selector-dropdown') => ({ + const groupSelector = (testSubj = 'group-selector-dropdown') => ({ async getElement() { return await testSubjects.find(testSubj); }, diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts index cbd13ae54bec4..0232af72d3e51 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts @@ -120,6 +120,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Findings Page - Grouping', function () { this.tags(['cloud_security_posture_findings_grouping']); let findings: typeof pageObjects.findings; + let groupSelector: ReturnType; before(async () => { findings = pageObjects.findings; @@ -133,6 +134,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await findings.navigateToLatestFindingsPage(); + groupSelector = await findings.groupSelector(); + await retry.waitFor( 'Findings table to be loaded', async () => (await findings.latestFindingsTable.getRowsCount()) === data.length @@ -141,12 +144,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await groupSelector.openDropDown(); + await groupSelector.setValue('None'); await findings.index.remove(); }); describe('Default Grouping', async () => { it('groups findings by resource and sort case sensitive asc', async () => { - const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Resource'); @@ -166,7 +170,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(unitCount).to.be('4 findings'); }); it('groups findings by rule name and sort case sensitive asc', async () => { - const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Rule name'); @@ -191,7 +194,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); it('groups findings by cloud account and sort case sensitive asc', async () => { - const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Cloud account'); const grouping = await findings.findingsGrouping(); @@ -210,7 +212,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); it('groups findings by Kubernetes cluster and sort case sensitive asc', async () => { - const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Kubernetes cluster'); const grouping = await findings.findingsGrouping(); @@ -231,10 +232,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('SearchBar', () => { it('add filter', async () => { - // Filter bar uses the field's customLabel in the DataView - const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Resource'); + // Filter bar uses the field's customLabel in the DataView await filterBar.addFilter({ field: 'Rule Name', operation: 'is', value: ruleName1 }); expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); From 975fd13919caa5fcf4cf77dc8c182b537c804e3c Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 15:22:55 -0800 Subject: [PATCH 24/63] update dashboard navigation --- .../dashboard_sections/summary_section.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index 50d5493387466..a0170705b1ec5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -21,11 +21,7 @@ import type { PosturePolicyTemplate, } from '../../../../common/types'; import { RisksTable } from '../compliance_charts/risks_table'; -import { - NavFilter, - useNavigateFindings, - useNavigateFindingsByResource, -} from '../../../common/hooks/use_navigate_findings'; +import { NavFilter, useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE, @@ -55,7 +51,6 @@ export const SummarySection = ({ complianceData: ComplianceDashboardData; }) => { const navToFindings = useNavigateFindings(); - const navToFindingsByResource = useNavigateFindingsByResource(); const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); @@ -121,7 +116,7 @@ export const SummarySection = ({ { - navToFindingsByResource(getPolicyTemplateQuery(dashboardType)); + navToFindings(getPolicyTemplateQuery(dashboardType)); }} > {i18n.translate( @@ -138,7 +133,7 @@ export const SummarySection = ({ cspmIntegrationLink, dashboardType, kspmIntegrationLink, - navToFindingsByResource, + navToFindings, ] ); From 1c0308c206af0da4364edd1c3e0847f19cffc164 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 15:39:58 -0800 Subject: [PATCH 25/63] fix ci errors --- .../src/containers/query/types.ts | 4 ++-- .../kbn-securitysolution-grouping/src/hooks/use_grouping.tsx | 2 +- .../cloud_security_data_table/cloud_security_data_table.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts index b208aef1e840b..69a284ecf7a59 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts @@ -19,11 +19,11 @@ type RunTimeMappings = | Record & { type: RuntimePrimitiveTypes }> | undefined; -interface BoolAgg { +export interface BoolAgg { bool: BoolQuery; } -interface RangeAgg { +export interface RangeAgg { range: { '@timestamp': { gte: string; lte: string } }; } diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index 997dd0895e35e..045a379209043 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -19,7 +19,7 @@ import { Grouping as GroupingComponent } from '../components/grouping'; /** Interface for grouping object where T is the `GroupingAggregation` * @interface GroupingArgs */ -interface Grouping { +export interface Grouping { getGrouping: (props: DynamicGroupingProps) => React.ReactElement; groupSelector: React.ReactElement; selectedGroups: string[]; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx index 71d18342890c3..50e81a0a0c7ec 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx @@ -228,7 +228,7 @@ export const CloudSecurityDataTable = ({ // Change the height of the grid to fit the page // If there are filters, leave space for the filter bar // Todo: Replace this component with EuiAutoSizer - height: height ?? `calc(100vh - ${filters.length > 0 ? 443 : 403}px)`, + height: height ?? `calc(100vh - ${filters?.length > 0 ? 443 : 403}px)`, }; const rowHeightState = 0; From 7bacb48ced3b32d8d4443944ecdbf606c8a7a911 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 15 Nov 2023 21:44:54 -0800 Subject: [PATCH 26/63] fixing ci and FTR tests --- .../src/hooks/use_grouping.tsx | 2 +- .../src/index.ts | 2 +- .../pages/findings.ts | 8 +++--- .../pages/findings_alerts.ts | 7 ++--- .../pages/findings_grouping.ts | 26 +++++++++---------- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index 045a379209043..997dd0895e35e 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -19,7 +19,7 @@ import { Grouping as GroupingComponent } from '../components/grouping'; /** Interface for grouping object where T is the `GroupingAggregation` * @interface GroupingArgs */ -export interface Grouping { +interface Grouping { getGrouping: (props: DynamicGroupingProps) => React.ReactElement; groupSelector: React.ReactElement; selectedGroups: string[]; diff --git a/packages/kbn-securitysolution-grouping/src/index.ts b/packages/kbn-securitysolution-grouping/src/index.ts index da07c1862da88..11f7058a155c9 100644 --- a/packages/kbn-securitysolution-grouping/src/index.ts +++ b/packages/kbn-securitysolution-grouping/src/index.ts @@ -9,5 +9,5 @@ export * from './components'; export * from './containers'; export * from './helpers'; -export * from './hooks/use_grouping'; +export { useGrouping, type DynamicGroupingProps } from './hooks/use_grouping'; export * from './hooks/types'; diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index 24ce9897d2169..e0ce367a53532 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -21,7 +21,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // We intentionally make some fields start with a capital letter to test that the query bar is case-insensitive/case-sensitive const data = [ { - '@timestamp': '1695819664234', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, rule: { @@ -38,7 +38,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'Upper case cluster id', }, { - '@timestamp': '1695819673242', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, rule: { @@ -55,7 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'Another Upper case cluster id', }, { - '@timestamp': '1695819676242', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `process`, sub_type: 'another lower case type' }, result: { evaluation: 'passed' }, rule: { @@ -72,7 +72,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'lower case cluster id', }, { - '@timestamp': '1695819680202', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `process`, sub_type: 'Upper case type again' }, result: { evaluation: 'failed' }, rule: { diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts index f346fc3d5c285..9b60c77c3ec15 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts @@ -19,6 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // We need to use a dataset for the tests to run const data = [ { + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, rule: { @@ -40,7 +41,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'Upper case cluster id', }, { - '@timestamp': '2023-09-10T14:01:00.000Z', + '@timestamp': new Date(Date.now() - 60 * 60 * 1000).toISOString(), resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, rule: { @@ -62,7 +63,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'Another Upper case cluster id', }, { - '@timestamp': '2023-09-10T14:02:00.000Z', + '@timestamp': new Date(Date.now() - 60 * 60 * 1000).toISOString(), resource: { id: chance.guid(), name: `process`, sub_type: 'another lower case type' }, result: { evaluation: 'passed' }, rule: { @@ -84,7 +85,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'lower case cluster id', }, { - '@timestamp': '2023-09-10T14:03:00.000Z', + '@timestamp': new Date(Date.now() - 60 * 60 * 1000).toISOString(), resource: { id: chance.guid(), name: `process`, sub_type: 'Upper case type again' }, result: { evaluation: 'failed' }, rule: { diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts index 0232af72d3e51..b6b535a571535 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts @@ -22,7 +22,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // We intentionally make some fields start with a capital letter to test that the query bar is case-insensitive/case-sensitive const data = [ { - '@timestamp': '1695819664234', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, orchestrator: { @@ -45,7 +45,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'Upper case cluster id', }, { - '@timestamp': '1695819673242', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, cloud: { @@ -68,7 +68,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'Another Upper case cluster id', }, { - '@timestamp': '1695819676242', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `process`, sub_type: 'another lower case type' }, result: { evaluation: 'passed' }, cloud: { @@ -91,7 +91,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { cluster_id: 'lower case cluster id', }, { - '@timestamp': '1695819680202', + '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `process`, sub_type: 'Upper case type again' }, result: { evaluation: 'failed' }, cloud: { @@ -120,7 +120,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Findings Page - Grouping', function () { this.tags(['cloud_security_posture_findings_grouping']); let findings: typeof pageObjects.findings; - let groupSelector: ReturnType; + // let groupSelector: ReturnType; before(async () => { findings = pageObjects.findings; @@ -133,17 +133,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await findings.index.add(data); await findings.navigateToLatestFindingsPage(); - - groupSelector = await findings.groupSelector(); - - await retry.waitFor( - 'Findings table to be loaded', - async () => (await findings.latestFindingsTable.getRowsCount()) === data.length - ); - pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.header.waitUntilLoadingHasFinished(); }); after(async () => { + const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('None'); await findings.index.remove(); @@ -151,6 +145,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Default Grouping', async () => { it('groups findings by resource and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Resource'); @@ -170,6 +165,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(unitCount).to.be('4 findings'); }); it('groups findings by rule name and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Rule name'); @@ -194,6 +190,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); it('groups findings by cloud account and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.setValue('Cloud account'); const grouping = await findings.findingsGrouping(); @@ -212,6 +210,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); it('groups findings by Kubernetes cluster and sort case sensitive asc', async () => { + const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Kubernetes cluster'); const grouping = await findings.findingsGrouping(); @@ -232,6 +231,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('SearchBar', () => { it('add filter', async () => { + const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Resource'); // Filter bar uses the field's customLabel in the DataView From ca4713c447e4df0dcb73a5974565b0000009c0f6 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 16 Nov 2023 01:04:13 -0800 Subject: [PATCH 27/63] fix CI types error --- .../kbn-securitysolution-grouping/src/hooks/use_grouping.tsx | 4 ++-- packages/kbn-securitysolution-grouping/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index 997dd0895e35e..3ff2232acbd72 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -19,7 +19,7 @@ import { Grouping as GroupingComponent } from '../components/grouping'; /** Interface for grouping object where T is the `GroupingAggregation` * @interface GroupingArgs */ -interface Grouping { +export interface UseGrouping { getGrouping: (props: DynamicGroupingProps) => React.ReactElement; groupSelector: React.ReactElement; selectedGroups: string[]; @@ -99,7 +99,7 @@ export const useGrouping = ({ onOptionsChange, tracker, title, -}: GroupingArgs): Grouping => { +}: GroupingArgs): UseGrouping => { const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState); const { activeGroups: selectedGroups } = useMemo( () => groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup, diff --git a/packages/kbn-securitysolution-grouping/src/index.ts b/packages/kbn-securitysolution-grouping/src/index.ts index 11f7058a155c9..da07c1862da88 100644 --- a/packages/kbn-securitysolution-grouping/src/index.ts +++ b/packages/kbn-securitysolution-grouping/src/index.ts @@ -9,5 +9,5 @@ export * from './components'; export * from './containers'; export * from './helpers'; -export { useGrouping, type DynamicGroupingProps } from './hooks/use_grouping'; +export * from './hooks/use_grouping'; export * from './hooks/types'; From 95f8cf49c80c36ba192328aa81b4957d49b0e3e1 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 16 Nov 2023 07:47:27 -0800 Subject: [PATCH 28/63] remove unused service from ftr --- .../cloud_security_posture_functional/pages/findings_grouping.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts index b6b535a571535..173630e56837e 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts @@ -14,7 +14,6 @@ import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const filterBar = getService('filterBar'); - const retry = getService('retry'); const pageObjects = getPageObjects(['common', 'findings', 'header']); const chance = new Chance(); From f3442d428f3e23798c71851368d171dde2ba4e76 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:49:09 -0800 Subject: [PATCH 29/63] add abbr utils --- .../common/utils/get_abbreviated_number.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts new file mode 100644 index 0000000000000..4bdb3525747ee --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts @@ -0,0 +1,18 @@ +/* + * 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 numeral from '@elastic/numeral'; + +/* + * Retrieve a number abbreviated in the following format: + thousand: 'k' + million: 'm' + billion: 'b' + trillion: 't' + */ +export const getAbbreviatedNumber = (value: number) => { + return value < 1000 ? value : numeral(value).format('0.0a'); +}; From dc766e43ef5086dc6a5f729f3595c9fbfd802530 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:49:21 -0800 Subject: [PATCH 30/63] table updates --- .../cloud_security_data_table/additional_controls.tsx | 8 ++------ .../components/cloud_security_data_table/use_styles.ts | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx index b1f79779e65e4..ff411d2dcd9e0 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx @@ -8,13 +8,9 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; import { type DataView } from '@kbn/data-views-plugin/common'; -import numeral from '@elastic/numeral'; import { FieldsSelectorModal } from './fields_selector'; import { useStyles } from './use_styles'; - -const formatNumber = (value: number) => { - return value < 1000 ? value : numeral(value).format('0.0a'); -}; +import { getAbbreviatedNumber } from '../../common/utils/get_abbreviated_number'; const GroupSelectorWrapper: React.FC = ({ children }) => { const styles = useStyles(); @@ -60,7 +56,7 @@ export const AdditionalControls = ({ /> )} - {`${formatNumber(total)} ${title}`} + {`${getAbbreviatedNumber(total)} ${title}`} { `; const groupBySelector = css` - width: 188px; margin-left: auto; `; From 9c82a65ec0f7d36801240d746fd00ec8ee4ac2bd Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:49:43 -0800 Subject: [PATCH 31/63] grouping updates --- .../cloud_security_grouping.tsx | 6 +++++- .../use_cloud_security_grouping.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx index 067046ce5457a..5d3e987fec5e5 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx @@ -37,9 +37,13 @@ export const CloudSecurityGrouping = ({
.euiFlexItem:last-child { display: none; } + && [data-test-subj='group-stats'] > .euiFlexItem:not(:first-child) > span { + border-right: none; + margin-right: 0; + } `} > {grouping.getGrouping({ diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index d2783af516e35..13de3f21b1889 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -8,6 +8,11 @@ import { useEffect, useMemo, useState } from 'react'; import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; import * as uuid from 'uuid'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { + GroupOption, + GroupPanelRenderer, + GroupStatsRenderer, +} from '@kbn/securitysolution-grouping/src'; import { useUrlQuery } from '../../common/hooks/use_url_query'; import { useBaseEsQuery, @@ -28,12 +33,16 @@ export const useCloudSecurityGrouping = ({ defaultGroupingOptions, getDefaultQuery, unit, + groupPanelRenderer, + groupStatsRenderer, }: { dataView: DataView; groupingTitle: string; - defaultGroupingOptions: Array<{ label: string; key: string }>; + defaultGroupingOptions: GroupOption[]; getDefaultQuery: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; unit: (count: number) => string; + groupPanelRenderer?: GroupPanelRenderer; + groupStatsRenderer?: GroupStatsRenderer; }) => { const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); @@ -57,6 +66,8 @@ export const useCloudSecurityGrouping = ({ const grouping = useGrouping({ componentProps: { unit, + groupPanelRenderer, + groupStatsRenderer, }, defaultGroupingOptions, fields: dataView.fields, From f94e901a6447224fbc36a96e2c0578209823fd11 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:50:06 -0800 Subject: [PATCH 32/63] add custom renderers --- .../latest_findings/constants.ts | 18 +- .../latest_findings_container.tsx | 84 ++++-- .../latest_findings_group_renderer.tsx | 252 ++++++++++++++++++ .../latest_findings/latest_findings_table.tsx | 2 +- .../latest_findings/use_grouped_findings.tsx | 25 ++ .../use_latest_findings_grouping.tsx | 131 ++++++++- 6 files changed, 480 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index 54884856fccf1..734e2f4c0ef5e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { GroupOption } from '@kbn/securitysolution-grouping'; import { FindingsBaseURLQuery } from '../../../common/types'; import { CloudSecurityDefaultColumn } from '../../../components/cloud_security_data_table'; @@ -15,30 +16,37 @@ export const FINDINGS_UNIT = (totalCount: number) => defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`, }); -export const defaultGroupingOptions = [ +export const GROUPING_OPTIONS = { + RESOURCE: 'resource.name', + RULE: 'rule.name', + CLOUD_ACCOUNT: 'cloud.account.name', + KUBERNETES: 'orchestrator.cluster.name', +}; + +export const defaultGroupingOptions: GroupOption[] = [ { label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { defaultMessage: 'Resource', }), - key: 'resource.name', + key: GROUPING_OPTIONS.RESOURCE, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { defaultMessage: 'Rule name', }), - key: 'rule.name', + key: GROUPING_OPTIONS.RULE, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { defaultMessage: 'Cloud account', }), - key: 'cloud.account.name', + key: GROUPING_OPTIONS.CLOUD_ACCOUNT, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', { defaultMessage: 'Kubernetes cluster', }), - key: 'orchestrator.cluster.name', + key: GROUPING_OPTIONS.KUBERNETES, }, ]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 613594c66e939..8ac7bd4687ab3 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback } from 'react'; import { Filter } from '@kbn/es-query'; +import { EuiSpacer } from '@elastic/eui'; import { defaultLoadingRenderer } from '../../../components/cloud_posture_page'; import { CloudSecurityGrouping } from '../../../components/cloud_security_grouping'; import type { FindingsBaseProps } from '../../../common/types'; @@ -13,6 +14,8 @@ import { FindingsSearchBar } from '../layout/findings_search_bar'; import { DEFAULT_TABLE_HEIGHT } from './constants'; import { useLatestFindingsGrouping } from './use_latest_findings_grouping'; import { LatestFindingsTable } from './latest_findings_table'; +import { groupPanelRenderer, groupStatsRenderer } from './latest_findings_group_renderer'; +import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { const renderChildComponent = useCallback( @@ -30,7 +33,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { ); const { - isGroupSelect, + isGroupSelected, groupData, grouping, isFetching, @@ -41,26 +44,69 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { onChangeGroupsPage, setUrlQuery, isGroupLoading, - } = useLatestFindingsGrouping({ dataView }); + } = useLatestFindingsGrouping({ dataView, groupPanelRenderer, groupStatsRenderer }); - if (isGroupSelect) { - return isGroupLoading ? ( - defaultLoadingRenderer() - ) : ( -
+ if (isGroupSelected) { + return ( + <> - -
+ {isGroupLoading ? ( +
+ + {}} passed={0} failed={0} /> + {defaultLoadingRenderer()} + {/* */} +
+ ) : ( +
+ + {}} + passed={groupData?.passedFindings?.doc_count} + failed={groupData?.failedFindings?.doc_count} + /> + +
+ )} + ); } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx new file mode 100644 index 0000000000000..62a54a62043dd --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -0,0 +1,252 @@ +/* + * 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 { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiIconTip, + EuiSkeletonTitle, + EuiText, + EuiTextBlockTruncate, + EuiToolTip, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { + ECSField, + GroupPanelRenderer, + RawBucket, + StatRenderer, +} from '@kbn/securitysolution-grouping/src'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_number'; +import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; +import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; +import { FindingsGroupingAggregation } from './use_grouped_findings'; +import { GROUPING_OPTIONS } from './constants'; + +/** + * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null. + */ +export function firstNonNullValue(valueOrCollection: ECSField): T | undefined { + if (valueOrCollection === null) { + return undefined; + } else if (Array.isArray(valueOrCollection)) { + for (const value of valueOrCollection) { + if (value !== null) { + return value; + } + } + } else { + return valueOrCollection; + } +} + +export const groupPanelRenderer: GroupPanelRenderer = ( + selectedGroup, + bucket, + nullGroupMessage, + isLoading +) => { + if (isLoading) { + return ( + + loading + + ); + } + + const benchmarkId = firstNonNullValue(bucket.benchmarkId?.buckets?.[0]?.key); + switch (selectedGroup) { + case GROUPING_OPTIONS.RESOURCE: + return ( + + + + + + + {bucket.key_as_string} {bucket.resourceName?.buckets?.[0].key} + + + + + + {bucket.resourceSubType?.buckets?.[0].key} + + + + + + ); + case GROUPING_OPTIONS.RULE: + return ( + + + + + + {bucket.key_as_string} + + + + + {firstNonNullValue(bucket.benchmarkName?.buckets?.[0].key)}{' '} + {firstNonNullValue(bucket.benchmarkVersion?.buckets?.[0].key)} + + + + + + ); + case GROUPING_OPTIONS.CLOUD_ACCOUNT: + return nullGroupMessage ? ( + <> + {' '} + _ + + + ) : ( + + {benchmarkId && ( + + + + )} + + + + + {bucket.key_as_string} + + + + + {bucket.benchmarkName?.buckets?.[0]?.key} + + + + + + ); + case GROUPING_OPTIONS.KUBERNETES: + return nullGroupMessage ? ( + <> + {' '} + _ + + + ) : ( + + {benchmarkId && ( + + + + )} + + + + + {bucket.key_as_string} + + + + + {bucket.benchmarkName?.buckets?.[0]?.key} + + + + + + ); + } +}; + +export const groupStatsRenderer = ( + selectedGroup: string, + bucket: RawBucket +): StatRenderer[] => { + const renderComplianceBar = () => { + const totalFailed = bucket.failedFindings?.doc_count || 0; + + const totalPassed = bucket.doc_count - totalFailed; + return ( + + + + + + + + + + + + + ); + }; + const renderFindingsCount = () => { + return ( + + + {getAbbreviatedNumber(bucket.doc_count)} + + + ); + }; + + const defaultBadges = [ + { + title: i18n.translate('xpack.csp.findings.grouping.stats.badges.findings', { + defaultMessage: 'Findings', + }), + renderer: renderFindingsCount(), + }, + { + title: '', + renderer: renderComplianceBar(), + }, + ]; + + return defaultBadges; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx index a66ce54fa99cc..be6d34a1df933 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx @@ -123,7 +123,7 @@ export const LatestFindingsTable = ({ passed={passed} failed={failed} /> - + )} ({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 137716967460f..451b85a5c8604 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -5,19 +5,105 @@ * 2.0. */ import { getGroupingQuery } from '@kbn/securitysolution-grouping'; -import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src'; +import { + GroupPanelRenderer, + GroupStatsRenderer, + isNoneGroup, + NamedAggregation, + parseGroupingQuery, +} from '@kbn/securitysolution-grouping/src'; import { useMemo } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; -import { useGroupedFindings } from './use_grouped_findings'; -import { FINDINGS_UNIT, groupingTitle, defaultGroupingOptions, getDefaultQuery } from './constants'; +import { FindingsGroupingAggregation, useGroupedFindings } from './use_grouped_findings'; +import { + FINDINGS_UNIT, + groupingTitle, + defaultGroupingOptions, + getDefaultQuery, + GROUPING_OPTIONS, +} from './constants'; import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; +const getTermAggregation = (key: keyof FindingsGroupingAggregation, field: string) => ({ + [key]: { + terms: { field, size: 1 }, + }, +}); + +const getAggregationsByGroupField = (field: string): NamedAggregation[] => { + if (isNoneGroup([field])) { + return []; + } + const aggMetrics: NamedAggregation[] = [ + { + failedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'failed' }, + }, + }, + }, + }, + ]; + + switch (field) { + case GROUPING_OPTIONS.RESOURCE: + return [ + ...aggMetrics, + getTermAggregation('resourceName', 'resource.id'), + getTermAggregation('resourceSubType', 'resource.sub_type'), + getTermAggregation('resourceType', 'resource.type'), + ]; + + case GROUPING_OPTIONS.RULE: + return [ + ...aggMetrics, + getTermAggregation('benchmarkName', 'rule.benchmark.name'), + getTermAggregation('benchmarkVersion', 'rule.benchmark.version'), + ]; + case GROUPING_OPTIONS.CLOUD_ACCOUNT: + return [ + ...aggMetrics, + getTermAggregation('benchmarkName', 'rule.benchmark.name'), + getTermAggregation('benchmarkId', 'rule.benchmark.id'), + ]; + case GROUPING_OPTIONS.KUBERNETES: + return [ + ...aggMetrics, + getTermAggregation('benchmarkName', 'rule.benchmark.name'), + getTermAggregation('benchmarkId', 'rule.benchmark.id'), + ]; + case 'resource.type': + return [ + ...aggMetrics, + getTermAggregation('resourceName', 'resource.id'), + getTermAggregation('resourceType', 'resource.type'), + ]; + case 'resource.sub_type': + return [ + ...aggMetrics, + getTermAggregation('resourceName', 'resource.id'), + getTermAggregation('resourceType', 'resource.type'), + getTermAggregation('resourceSubType', 'resource.sub_type'), + ]; + } + return aggMetrics; +}; + /** * Utility hook to get the latest findings grouping data * for the findings page */ -export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) => { +export const useLatestFindingsGrouping = ({ + dataView, + groupPanelRenderer, + groupStatsRenderer, +}: { + dataView: DataView; + groupPanelRenderer?: GroupPanelRenderer; + groupStatsRenderer?: GroupStatsRenderer; +}) => { const { activePageIndex, grouping, @@ -35,17 +121,48 @@ export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) defaultGroupingOptions, getDefaultQuery, unit: FINDINGS_UNIT, + groupPanelRenderer, + groupStatsRenderer, }); const groupingQuery = getGroupingQuery({ - additionalFilters: [query], + additionalFilters: [ + query, + // { + // bool: { + // must: [], + // must_not: [], + // should: [], + // filter: [{ exists: { field: selectedGroup } }], + // }, + // }, + ], groupByField: selectedGroup, uniqueValue, from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, to: 'now', pageNumber: activePageIndex * pageSize, size: pageSize, - sort: [{ _key: { order: 'asc' } }], + sort: [{ _count: { order: 'desc' } }, { _key: { order: 'asc' } }], + statsAggregations: getAggregationsByGroupField(selectedGroup), + rootAggregations: [ + { + failedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'failed' }, + }, + }, + }, + passedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'passed' }, + }, + }, + }, + }, + ], }); const { data, isFetching } = useGroupedFindings({ @@ -68,7 +185,7 @@ export const useLatestFindingsGrouping = ({ dataView }: { dataView: DataView }) onChangeGroupsItemsPerPage, onChangeGroupsPage, setUrlQuery, - isGroupSelect: !isNoneSelected, + isGroupSelected: !isNoneSelected, isGroupLoading: !data, }; }; From ddb30ee20d7c3ef254979b31927d25300039fccf Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:50:24 -0800 Subject: [PATCH 33/63] swap findings tabs --- .../public/pages/findings/findings.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx index 966f95cb367fa..67e46d82020cc 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx @@ -99,16 +99,6 @@ export const Findings = () => { - - - { defaultMessage="Misconfigurations" /> + + + )} From 1db390514f9b19a8e871a7087d9236efc85c6db1 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:51:21 -0800 Subject: [PATCH 34/63] compliance bar background --- .../pages/configurations/layout/findings_distribution_bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx index 294b05d9dd802..1f1241c4d1dfa 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx @@ -113,7 +113,7 @@ const DistributionBar: React.FC> = ({ gutterSize="none" css={css` height: 8px; - background: ${euiTheme.colors.subduedText}; + background: ${euiTheme.colors.lightestShade}; `} > Date: Wed, 22 Nov 2023 14:51:40 -0800 Subject: [PATCH 35/63] add isLoading to grouping renderer --- .../kbn-securitysolution-grouping/src/components/grouping.tsx | 2 +- packages/kbn-securitysolution-grouping/src/components/types.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx index aab42a0804e4c..5ae1037d9edb3 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx @@ -128,7 +128,7 @@ const GroupingComponent = ({ groupBucket={groupBucket} groupPanelRenderer={ groupPanelRenderer && - groupPanelRenderer(selectedGroup, groupBucket, nullGroupMessage) + groupPanelRenderer(selectedGroup, groupBucket, nullGroupMessage, isLoading) } isLoading={isLoading} onToggleGroup={(isOpen) => { diff --git a/packages/kbn-securitysolution-grouping/src/components/types.ts b/packages/kbn-securitysolution-grouping/src/components/types.ts index 6987a09c083f8..43a9af13372f7 100644 --- a/packages/kbn-securitysolution-grouping/src/components/types.ts +++ b/packages/kbn-securitysolution-grouping/src/components/types.ts @@ -76,7 +76,8 @@ export type GroupStatsRenderer = ( export type GroupPanelRenderer = ( selectedGroup: string, fieldBucket: RawBucket, - nullGroupMessage?: string + nullGroupMessage?: string, + isLoading?: boolean ) => JSX.Element | undefined; export type OnGroupToggle = (params: { From 8b34e143d7a95bb421d5c4befc21e9afd0c3534e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 22 Nov 2023 14:51:54 -0800 Subject: [PATCH 36/63] add reverse compliance bar --- .../components/compliance_score_bar.tsx | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx index 8f866079ab160..02abc15fa1573 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx @@ -18,12 +18,18 @@ import { statusColors } from '../common/constants'; export const ComplianceScoreBar = ({ totalPassed, totalFailed, + isReverse = false, + size = 'm', }: { totalPassed: number; totalFailed: number; + isReverse?: boolean; + size?: 'm' | 'l'; }) => { const { euiTheme } = useEuiTheme(); - const complianceScore = calculatePostureScore(totalPassed, totalFailed); + const complianceScore = isReverse + ? calculatePostureScore(totalFailed, totalPassed) + : calculatePostureScore(totalPassed, totalFailed); return ( - + {!!totalPassed && ( )} {!!totalFailed && ( )} - + {`${complianceScore.toFixed(0)}%`} From 82493cf594880b5576d7a6b6f2341629a7ce51d2 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 24 Nov 2023 21:07:57 -0800 Subject: [PATCH 37/63] reverting error handling --- .../common/hooks/use_cloud_posture_data_table/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts index e86d2a77589b0..576c15aa18e7c 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts @@ -28,7 +28,10 @@ const getBaseQuery = ({ query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query }; } catch (error) { - throw new Error(error); + return { + query: undefined, + error: error instanceof Error ? error : new Error('Unknown Error'), + }; } }; From 316a00f31f8ce5445457ee43747783619d6ec49f Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 24 Nov 2023 21:08:20 -0800 Subject: [PATCH 38/63] implementing reset filters --- .../cloud_security_grouping.tsx | 10 +++++++++- .../use_cloud_security_grouping.ts | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx index 5d3e987fec5e5..30a5ba20c37ba 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx @@ -9,6 +9,8 @@ import { ParsedGroupingAggregation } from '@kbn/securitysolution-grouping/src'; import { Filter } from '@kbn/es-query'; import React from 'react'; import { css } from '@emotion/react'; +import { EmptyState } from '../empty_state'; +import { CSP_GROUPING } from '../test_subjects'; interface CloudSecurityGroupingProps { data: ParsedGroupingAggregation; @@ -20,6 +22,7 @@ interface CloudSecurityGroupingProps { onChangeGroupsItemsPerPage: (size: number) => void; onChangeGroupsPage: (index: number) => void; selectedGroup: string; + onResetFilters: () => void; } export const CloudSecurityGrouping = ({ @@ -32,10 +35,15 @@ export const CloudSecurityGrouping = ({ onChangeGroupsItemsPerPage, onChangeGroupsPage, selectedGroup, + onResetFilters, }: CloudSecurityGroupingProps) => { + if (!isFetching && !data.unitsCount?.value) { + return ; + } + return (
.euiFlexItem:last-child { display: none; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index 13de3f21b1889..64307bd3e941a 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; import * as uuid from 'uuid'; import type { DataView } from '@kbn/data-views-plugin/common'; @@ -49,7 +49,7 @@ export const useCloudSecurityGrouping = ({ const [activePageIndex, setActivePageIndex] = useState(0); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const { query } = useBaseEsQuery({ + const { query, error } = useBaseEsQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query, @@ -92,6 +92,16 @@ export const useCloudSecurityGrouping = ({ setPageSize(size); }; + const onResetFilters = useCallback(() => { + setUrlQuery({ + filters: [], + query: { + query: '', + language: 'kuery', + }, + }); + }, [setUrlQuery]); + const onChangeGroupsPage = (index: number) => setActivePageIndex(index); return { @@ -99,11 +109,13 @@ export const useCloudSecurityGrouping = ({ grouping, pageSize, query, + error, selectedGroup, setUrlQuery, uniqueValue, isNoneSelected, onChangeGroupsItemsPerPage, onChangeGroupsPage, + onResetFilters, }; }; From e4b4bf8eb8020a9b7e0066a05b0d463381f13de5 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 24 Nov 2023 21:08:38 -0800 Subject: [PATCH 39/63] adding test subjects --- .../cloud_security_posture/public/components/test_subjects.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 91589cee3dcfc..7afd5c8bc9a60 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -39,3 +39,5 @@ export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score export const TAKE_ACTION_SUBJ = 'csp:take_action'; export const CREATE_RULE_ACTION_SUBJ = 'csp:create_rule'; + +export const CSP_GROUPING = 'cloudSecurityGrouping'; From a4f8c29e82741465ae3d9ff2158e19de6296cda4 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 27 Nov 2023 11:35:31 -0800 Subject: [PATCH 40/63] add error handling --- .../latest_findings_container.tsx | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 8ac7bd4687ab3..2a2dccdc77a1b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -16,6 +16,7 @@ import { useLatestFindingsGrouping } from './use_latest_findings_grouping'; import { LatestFindingsTable } from './latest_findings_table'; import { groupPanelRenderer, groupStatsRenderer } from './latest_findings_group_renderer'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; +import { ErrorCallout } from '../layout/error_callout'; export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { const renderChildComponent = useCallback( @@ -44,8 +45,20 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { onChangeGroupsPage, setUrlQuery, isGroupLoading, + onResetFilters, + error, } = useLatestFindingsGrouping({ dataView, groupPanelRenderer, groupStatsRenderer }); + if (error) { + return ( + <> + + + + + ); + } + if (isGroupSelected) { return ( <> @@ -55,35 +68,6 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { {}} passed={0} failed={0} /> {defaultLoadingRenderer()} - {/* */}
) : (
@@ -103,6 +87,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { isFetching={isFetching} pageSize={pageSize} selectedGroup={selectedGroup} + onResetFilters={onResetFilters} />
)} From d7936166c9fb057d52d83ce4b9364cb3cbbe6dd3 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 27 Nov 2023 11:36:07 -0800 Subject: [PATCH 41/63] sorting groups by compliance score --- .../use_latest_findings_grouping.tsx | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 451b85a5c8604..162e20bae43fa 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -37,6 +37,11 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { } const aggMetrics: NamedAggregation[] = [ { + groupByField: { + cardinality: { + field, + }, + }, failedFindings: { filter: { term: { @@ -44,6 +49,22 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { }, }, }, + passedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'passed' }, + }, + }, + }, + complianceScore: { + bucket_script: { + buckets_path: { + passed: 'passedFindings>_count', + failed: 'failedFindings>_count', + }, + script: 'params.passed / (params.passed + params.failed)', + }, + }, }, ]; @@ -115,6 +136,8 @@ export const useLatestFindingsGrouping = ({ setUrlQuery, uniqueValue, isNoneSelected, + onResetFilters, + error, } = useCloudSecurityGrouping({ dataView, groupingTitle, @@ -126,24 +149,14 @@ export const useLatestFindingsGrouping = ({ }); const groupingQuery = getGroupingQuery({ - additionalFilters: [ - query, - // { - // bool: { - // must: [], - // must_not: [], - // should: [], - // filter: [{ exists: { field: selectedGroup } }], - // }, - // }, - ], + additionalFilters: query ? [query] : [], groupByField: selectedGroup, uniqueValue, from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, to: 'now', pageNumber: activePageIndex * pageSize, size: pageSize, - sort: [{ _count: { order: 'desc' } }, { _key: { order: 'asc' } }], + sort: [{ groupByField: { order: 'desc' } }, { complianceScore: { order: 'asc' } }], statsAggregations: getAggregationsByGroupField(selectedGroup), rootAggregations: [ { @@ -187,5 +200,7 @@ export const useLatestFindingsGrouping = ({ setUrlQuery, isGroupSelected: !isNoneSelected, isGroupLoading: !data, + onResetFilters, + error, }; }; From 166655fda11fa44703290edb31a5da210aa237c7 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 27 Nov 2023 11:36:24 -0800 Subject: [PATCH 42/63] custom non matching groups for cloud and kubernetes --- .../latest_findings_group_renderer.tsx | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index 62a54a62043dd..4a3d792786c84 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -8,7 +8,6 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiIconTip, EuiSkeletonTitle, EuiText, @@ -25,7 +24,6 @@ import { import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_number'; import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; @@ -112,11 +110,18 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.CLOUD_ACCOUNT: return nullGroupMessage ? ( - <> - {' '} - _ +
+ No Cloud account - +
) : ( {benchmarkId && ( @@ -145,11 +150,18 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.KUBERNETES: return nullGroupMessage ? ( - <> - {' '} - _ +
+ No Kubernetes cluster - +
) : ( {benchmarkId && ( From 6ff86abf3b85a2de97658e942e4f6ab6d4980693 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 27 Nov 2023 15:27:28 -0800 Subject: [PATCH 43/63] adding maxGroupingLevels tests --- .../components/group_selector/index.test.tsx | 48 +++++++++++++++++++ .../src/hooks/use_get_group_selector.test.tsx | 28 +++++++++++ 2 files changed, 76 insertions(+) diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx index 2172390f41e85..9f09e3b26ea49 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx @@ -124,4 +124,52 @@ describe('group selector', () => { ); expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); }); + describe('when maxGroupingLevels is 1', () => { + it('Presents single option selector label when dropdown is clicked', () => { + const { getByTestId } = render( + + ); + fireEvent.click(getByTestId('group-selector-dropdown')); + expect(getByTestId('contextMenuPanelTitle').textContent).toMatch(/select grouping/i); + }); + it('Does not disable any options when maxGroupingLevels is 1 and one option is selected', () => { + const groupSelected = ['kibana.alert.rule.name']; + + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('group-selector-dropdown')); + + [...testProps.options, { key: 'custom', label: 'Custom field' }].forEach((o) => { + expect(getByTestId(`panel-${o.key}`)).not.toHaveAttribute('disabled'); + }); + }); + }); + describe('when maxGroupingLevels is greater than 1', () => { + it('Presents select up to "X" groupings when dropdown is clicked', () => { + const { getByTestId } = render( + + ); + fireEvent.click(getByTestId('group-selector-dropdown')); + expect(getByTestId('contextMenuPanelTitle').textContent).toMatch(/select up to 3 groupings/i); + }); + it('Disables non-selected options when maxGroupingLevels is greater than 1 and the selects items reaches the maxGroupingLevels', () => { + const groupSelected = ['kibana.alert.rule.name', 'user.name']; + + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('group-selector-dropdown')); + + [...testProps.options, { key: 'custom', label: 'Custom field' }].forEach((o) => { + if (groupSelected.includes(o.key) || o.key === 'none') { + expect(getByTestId(`panel-${o.key}`)).not.toHaveAttribute('disabled'); + } else { + expect(getByTestId(`panel-${o.key}`)).toHaveAttribute('disabled'); + } + }); + }); + }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx index 36fe9badac55d..e6221c677b9d5 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx @@ -157,6 +157,34 @@ describe('Group Selector Hooks', () => { }); }); + it('On group change when maxGroupingLevels is 1, remove previously selected group', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name'], + }, + }; + const { result } = renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + maxGroupingLevels: 1, + groupingState: { + groupById: testGroup, + }, + }, + }); + act(() => result.current.props.onGroupChange('user.name')); + + expect(dispatch).toHaveBeenCalledWith({ + payload: { + id: groupingId, + activeGroups: ['user.name'], + }, + type: ActionType.updateActiveGroups, + }); + }); + it('On group change, resets active page, sets active group, and leaves options alone', () => { const testGroup = { [groupingId]: { From ad6a3e86d1445965d99988aa996645828be05f41 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 29 Nov 2023 07:49:15 -0800 Subject: [PATCH 44/63] fix wrong merge --- .../cloud_security_data_table/additional_controls.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx index 1895b8f731981..ff411d2dcd9e0 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx @@ -22,16 +22,6 @@ const GroupSelectorWrapper: React.FC = ({ children }) => { ); }; -const GroupSelectorWrapper: React.FC = ({ children }) => { - const styles = useStyles(); - - return ( - - {children} - - ); -}; - export const AdditionalControls = ({ total, title, From 9b74174e87edcac7ce161972c607bce39b6bda02 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 29 Nov 2023 07:49:44 -0800 Subject: [PATCH 45/63] reverting isReverse prop --- .../public/components/compliance_score_bar.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx index 02abc15fa1573..87c9263467a9f 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx @@ -18,18 +18,14 @@ import { statusColors } from '../common/constants'; export const ComplianceScoreBar = ({ totalPassed, totalFailed, - isReverse = false, size = 'm', }: { totalPassed: number; totalFailed: number; - isReverse?: boolean; size?: 'm' | 'l'; }) => { const { euiTheme } = useEuiTheme(); - const complianceScore = isReverse - ? calculatePostureScore(totalFailed, totalPassed) - : calculatePostureScore(totalPassed, totalFailed); + const complianceScore = calculatePostureScore(totalPassed, totalFailed); return ( Date: Wed, 29 Nov 2023 07:50:33 -0800 Subject: [PATCH 46/63] translations --- .../latest_findings/constants.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index 734e2f4c0ef5e..fcabc99fed089 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -18,11 +18,33 @@ export const FINDINGS_UNIT = (totalCount: number) => export const GROUPING_OPTIONS = { RESOURCE: 'resource.name', - RULE: 'rule.name', + RULE_NAME: 'rule.name', CLOUD_ACCOUNT: 'cloud.account.name', KUBERNETES: 'orchestrator.cluster.name', }; +export const NULL_GROUPING_UNIT = i18n.translate('xpack.csp.findings.grouping.nullGroupUnit', { + defaultMessage: 'findings', +}); + +export const NULL_GROUPING_MESSAGES = { + RESOURCE: i18n.translate('xpack.csp.findings.grouping.resource.nullGroupTitle', { + defaultMessage: 'No resource', + }), + RULE_NAME: i18n.translate('xpack.csp.findings.grouping.rule.nullGroupTitle', { + defaultMessage: 'No rule', + }), + CLOUD_ACCOUNT: i18n.translate('xpack.csp.findings.grouping.cloudAccount.nullGroupTitle', { + defaultMessage: 'No cloud account', + }), + KUBERNETES: i18n.translate('xpack.csp.findings.grouping.kubernetes.nullGroupTitle', { + defaultMessage: 'No Kubernetes cluster', + }), + GENERIC_MESSAGE: i18n.translate('xpack.csp.findings.grouping.kubernetes.nullGroupTitle', { + defaultMessage: 'No grouping', + }), +}; + export const defaultGroupingOptions: GroupOption[] = [ { label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { @@ -34,7 +56,7 @@ export const defaultGroupingOptions: GroupOption[] = [ label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { defaultMessage: 'Rule name', }), - key: GROUPING_OPTIONS.RULE, + key: GROUPING_OPTIONS.RULE_NAME, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { From 70b5c82544ab29c1897b92a3c3e33b9d7b93d700 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 29 Nov 2023 07:50:53 -0800 Subject: [PATCH 47/63] non matching group custom renderer --- .../latest_findings_group_renderer.tsx | 133 ++++++++++++------ 1 file changed, 93 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index 4a3d792786c84..acd803a348f00 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -28,7 +28,7 @@ import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_numb import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; import { FindingsGroupingAggregation } from './use_grouped_findings'; -import { GROUPING_OPTIONS } from './constants'; +import { GROUPING_OPTIONS, NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT } from './constants'; /** * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null. @@ -47,6 +47,55 @@ export function firstNonNullValue(valueOrCollection: ECSField): T | undefi } } +const NullGroupComponent = ({ + title, + field, + unit = NULL_GROUPING_UNIT, +}: { + title: string; + field: string; + unit?: string; +}) => { + return ( +
+ {title} + + + + + ), + field: {field}, + unit, + }} + /> + + } + position="right" + /> +
+ ); +}; + +// const InlineFlexItem = ({ children }: { children: React.ReactNode }) => ( + export const groupPanelRenderer: GroupPanelRenderer = ( selectedGroup, bucket, @@ -64,15 +113,23 @@ export const groupPanelRenderer: GroupPanelRenderer const benchmarkId = firstNonNullValue(bucket.benchmarkId?.buckets?.[0]?.key); switch (selectedGroup) { case GROUPING_OPTIONS.RESOURCE: - return ( + return nullGroupMessage ? ( + + ) : ( - + {bucket.key_as_string} {bucket.resourceName?.buckets?.[0].key} @@ -88,8 +145,10 @@ export const groupPanelRenderer: GroupPanelRenderer ); - case GROUPING_OPTIONS.RULE: - return ( + case GROUPING_OPTIONS.RULE_NAME: + return nullGroupMessage ? ( + + ) : ( @@ -110,22 +169,16 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.CLOUD_ACCOUNT: return nullGroupMessage ? ( -
- No Cloud account - -
+ ) : ( {benchmarkId && ( - + )} - + {bucket.key_as_string} @@ -150,18 +203,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.KUBERNETES: return nullGroupMessage ? ( -
- No Kubernetes cluster - -
+ ) : ( {benchmarkId && ( @@ -188,6 +230,22 @@ export const groupPanelRenderer: GroupPanelRenderer
); + default: + return nullGroupMessage ? ( + + ) : ( + + + + + + {bucket.key_as_string} + + + + + + ); } }; @@ -215,19 +273,14 @@ export const groupStatsRenderer = (
- +
); From feea7798abccd707e839f69d05b9befb938730da Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 29 Nov 2023 07:52:35 -0800 Subject: [PATCH 48/63] adding Distribution Bar Click handler --- .../use_cloud_security_grouping.ts | 1 + .../latest_findings_container.tsx | 31 ++++++----- .../latest_findings/use_grouped_findings.tsx | 20 +++++-- .../use_latest_findings_grouping.tsx | 54 +++++++++++++++++-- 4 files changed, 82 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index 64307bd3e941a..e3a329a6678fc 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -117,5 +117,6 @@ export const useCloudSecurityGrouping = ({ onChangeGroupsItemsPerPage, onChangeGroupsPage, onResetFilters, + filters: urlQuery.filters, }; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 2a2dccdc77a1b..93a883fbe0021 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -47,6 +47,9 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { isGroupLoading, onResetFilters, error, + totalPassedFindings, + onDistributionBarClick, + totalFailedFindings, } = useLatestFindingsGrouping({ dataView, groupPanelRenderer, groupStatsRenderer }); if (error) { @@ -63,20 +66,16 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { return ( <> - {isGroupLoading ? ( -
- - {}} passed={0} failed={0} /> - {defaultLoadingRenderer()} -
- ) : ( -
- - {}} - passed={groupData?.passedFindings?.doc_count} - failed={groupData?.failedFindings?.doc_count} - /> +
+ + + {isGroupLoading ? ( + defaultLoadingRenderer() + ) : ( { selectedGroup={selectedGroup} onResetFilters={onResetFilters} /> -
- )} + )} +
); } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx index 5cc7e378657e3..c67c1250d1be7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx @@ -7,8 +7,7 @@ import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; -import { GroupingAggregation } from '@kbn/securitysolution-grouping'; -import { GenericBuckets, GroupingQuery } from '@kbn/securitysolution-grouping/src'; +import { GenericBuckets, GroupingQuery, RootAggregation } from '@kbn/securitysolution-grouping/src'; import { useQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; @@ -18,6 +17,16 @@ import { showErrorToast } from '../../../common/utils/show_error_toast'; // Elasticsearch returns `null` when a sub-aggregation cannot be computed type NumberOrNull = number | null; +export interface FindingsRootGroupingAggregation + extends RootAggregation { + failedFindings?: { + doc_count?: NumberOrNull; + }; + passedFindings?: { + doc_count?: NumberOrNull; + }; +} + export interface FindingsGroupingAggregation { unitsCount?: { value?: NumberOrNull; @@ -28,6 +37,9 @@ export interface FindingsGroupingAggregation { failedFindings?: { doc_count?: NumberOrNull; }; + passedFindings?: { + doc_count?: NumberOrNull; + }; groupByFields?: { buckets?: GenericBuckets[]; }; @@ -81,9 +93,7 @@ export const useGroupedFindings = ({ } = await lastValueFrom( data.search.search< {}, - IKibanaSearchResponse< - SearchResponse<{}, GroupingAggregation> - > + IKibanaSearchResponse> >({ params: getGroupedFindingsQuery(query), }) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 162e20bae43fa..a60d96694e395 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -6,6 +6,7 @@ */ import { getGroupingQuery } from '@kbn/securitysolution-grouping'; import { + GroupingAggregation, GroupPanelRenderer, GroupStatsRenderer, isNoneGroup, @@ -14,8 +15,13 @@ import { } from '@kbn/securitysolution-grouping/src'; import { useMemo } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; +import { Evaluation } from '../../../../common/types'; import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; -import { FindingsGroupingAggregation, useGroupedFindings } from './use_grouped_findings'; +import { + FindingsGroupingAggregation, + FindingsRootGroupingAggregation, + useGroupedFindings, +} from './use_grouped_findings'; import { FINDINGS_UNIT, groupingTitle, @@ -24,6 +30,7 @@ import { GROUPING_OPTIONS, } from './constants'; import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; +import { getFilters } from '../utils/get_filters'; const getTermAggregation = (key: keyof FindingsGroupingAggregation, field: string) => ({ [key]: { @@ -77,7 +84,7 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { getTermAggregation('resourceType', 'resource.type'), ]; - case GROUPING_OPTIONS.RULE: + case GROUPING_OPTIONS.RULE_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), @@ -112,6 +119,18 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { return aggMetrics; }; +/** + * Type Guard for checking if the given source is a FindingsRootGroupingAggregation + */ +const isFindingsRootGroupingAggregation = ( + groupData: Record | undefined +): groupData is FindingsRootGroupingAggregation => { + return ( + groupData?.passedFindings?.doc_count !== undefined && + groupData?.failedFindings?.doc_count !== undefined + ); +}; + /** * Utility hook to get the latest findings grouping data * for the findings page @@ -138,6 +157,7 @@ export const useLatestFindingsGrouping = ({ isNoneSelected, onResetFilters, error, + filters, } = useCloudSecurityGrouping({ dataView, groupingTitle, @@ -184,10 +204,34 @@ export const useLatestFindingsGrouping = ({ }); const groupData = useMemo( - () => parseGroupingQuery(selectedGroup, uniqueValue, data), + () => + parseGroupingQuery( + selectedGroup, + uniqueValue, + data as GroupingAggregation + ), [data, selectedGroup, uniqueValue] ); + const totalPassedFindings = isFindingsRootGroupingAggregation(groupData) + ? groupData?.passedFindings?.doc_count || 0 + : 0; + const totalFailedFindings = isFindingsRootGroupingAggregation(groupData) + ? groupData?.failedFindings?.doc_count || 0 + : 0; + + const onDistributionBarClick = (evaluation: Evaluation) => { + setUrlQuery({ + filters: getFilters({ + filters, + dataView, + field: 'result.evaluation', + value: evaluation, + negate: false, + }), + }); + }; + return { groupData, grouping, @@ -201,6 +245,10 @@ export const useLatestFindingsGrouping = ({ isGroupSelected: !isNoneSelected, isGroupLoading: !data, onResetFilters, + filters, error, + onDistributionBarClick, + totalPassedFindings, + totalFailedFindings, }; }; From 6e1a5f19827cc8cbe6772aed787b4ae3c4977694 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 29 Nov 2023 10:28:21 -0800 Subject: [PATCH 49/63] remove unused --- .../latest_findings_group_renderer.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index acd803a348f00..85f58ae68f402 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -94,8 +94,6 @@ const NullGroupComponent = ({ ); }; -// const InlineFlexItem = ({ children }: { children: React.ReactNode }) => ( - export const groupPanelRenderer: GroupPanelRenderer = ( selectedGroup, bucket, @@ -207,7 +205,12 @@ export const groupPanelRenderer: GroupPanelRenderer ) : ( {benchmarkId && ( - + )} - + {bucket.key_as_string} @@ -259,9 +262,9 @@ export const groupStatsRenderer = ( const totalPassed = bucket.doc_count - totalFailed; return ( Date: Wed, 29 Nov 2023 18:22:26 -0800 Subject: [PATCH 50/63] revert isLoading --- .../src/components/grouping.tsx | 2 +- .../src/components/types.ts | 3 +- .../latest_findings_group_renderer.tsx | 29 +++++-------------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx index 5ae1037d9edb3..aab42a0804e4c 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx @@ -128,7 +128,7 @@ const GroupingComponent = ({ groupBucket={groupBucket} groupPanelRenderer={ groupPanelRenderer && - groupPanelRenderer(selectedGroup, groupBucket, nullGroupMessage, isLoading) + groupPanelRenderer(selectedGroup, groupBucket, nullGroupMessage) } isLoading={isLoading} onToggleGroup={(isOpen) => { diff --git a/packages/kbn-securitysolution-grouping/src/components/types.ts b/packages/kbn-securitysolution-grouping/src/components/types.ts index 43a9af13372f7..6987a09c083f8 100644 --- a/packages/kbn-securitysolution-grouping/src/components/types.ts +++ b/packages/kbn-securitysolution-grouping/src/components/types.ts @@ -76,8 +76,7 @@ export type GroupStatsRenderer = ( export type GroupPanelRenderer = ( selectedGroup: string, fieldBucket: RawBucket, - nullGroupMessage?: string, - isLoading?: boolean + nullGroupMessage?: string ) => JSX.Element | undefined; export type OnGroupToggle = (params: { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index 85f58ae68f402..dfde3c9b4a653 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -9,7 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, - EuiSkeletonTitle, EuiText, EuiTextBlockTruncate, EuiToolTip, @@ -57,17 +56,14 @@ const NullGroupComponent = ({ unit?: string; }) => { return ( -
+ {title} -
+
); }; export const groupPanelRenderer: GroupPanelRenderer = ( selectedGroup, bucket, - nullGroupMessage, - isLoading + nullGroupMessage ) => { - if (isLoading) { - return ( - - loading - - ); - } - const benchmarkId = firstNonNullValue(bucket.benchmarkId?.buckets?.[0]?.key); switch (selectedGroup) { case GROUPING_OPTIONS.RESOURCE: From f3165890f94d5c69f4ad9a7de5cda3a8a38eb259 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Wed, 29 Nov 2023 21:01:50 -0800 Subject: [PATCH 51/63] updating custom renderers css --- .../components/compliance_score_bar.tsx | 14 ++- .../latest_findings_group_renderer.tsx | 107 ++++++++---------- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx index 87c9263467a9f..cc155b4a42aba 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { css, SerializedStyles } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { calculatePostureScore } from '../../common/utils/helpers'; @@ -19,21 +19,25 @@ export const ComplianceScoreBar = ({ totalPassed, totalFailed, size = 'm', + overrideCss, }: { totalPassed: number; totalFailed: number; size?: 'm' | 'l'; + overrideCss?: SerializedStyles; }) => { const { euiTheme } = useEuiTheme(); const complianceScore = calculatePostureScore(totalPassed, totalFailed); + // ensures the compliance bar takes full width of its parent + const fullWidthTooltipCss = css` + width: 100%; + `; + return ( ) : ( {benchmarkId && ( - + ) : ( {benchmarkId && ( - + } }; -export const groupStatsRenderer = ( - selectedGroup: string, - bucket: RawBucket -): StatRenderer[] => { - const renderComplianceBar = () => { - const totalFailed = bucket.failedFindings?.doc_count || 0; +const FindingsCountComponent = ({ bucket }: { bucket: RawBucket }) => { + const { euiTheme } = useEuiTheme(); - const totalPassed = bucket.doc_count - totalFailed; - return ( - + - - - - - - - - - - - - ); - }; - const renderFindingsCount = () => { - return ( - - - {getAbbreviatedNumber(bucket.doc_count)} - - - ); - }; + {getAbbreviatedNumber(bucket.doc_count)} + + + ); +}; + +const FindingsCount = React.memo(FindingsCountComponent); + +const ComplianceBarComponent = ({ bucket }: { bucket: RawBucket }) => { + const { euiTheme } = useEuiTheme(); + const totalFailed = bucket.failedFindings?.doc_count || 0; + const totalPassed = bucket.doc_count - totalFailed; + return ( + + ); +}; + +const ComplianceBar = React.memo(ComplianceBarComponent); + +export const groupStatsRenderer = ( + selectedGroup: string, + bucket: RawBucket +): StatRenderer[] => { const defaultBadges = [ { title: i18n.translate('xpack.csp.findings.grouping.stats.badges.findings', { defaultMessage: 'Findings', }), - renderer: renderFindingsCount(), + renderer: , }, { - title: '', - renderer: renderComplianceBar(), + title: i18n.translate('xpack.csp.findings.grouping.stats.badges.compliance', { + defaultMessage: 'Compliance', + }), + renderer: , }, ]; From c940a9d00ac8ac31e29534be5cf0908746641c6d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 30 Nov 2023 08:34:21 -0800 Subject: [PATCH 52/63] wip updating ftr tests --- .../pages/findings_grouping.ts | 78 +++++++++++++++---- .../pages/index.ts | 14 ++-- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts index 173630e56837e..6d432f686a1eb 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts @@ -23,7 +23,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { { '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, - result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + result: { evaluation: 'passed' }, orchestrator: { cluster: { id: '1', @@ -46,7 +46,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { { '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, - result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + result: { evaluation: 'passed' }, cloud: { account: { id: '1', @@ -143,19 +143,44 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('Default Grouping', async () => { - it('groups findings by resource and sort case sensitive asc', async () => { + it('groups findings by resource and sort by failed findings desc', async () => { const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Resource'); const grouping = await findings.findingsGrouping(); - const resourceOrder = ['Pod', 'kubelet', 'process']; + const resourceOrder = [ + { + resourceName: 'process', + resourceId: data[2].resource.id, + resourceSubType: data[2].resource.sub_type, + findingsCount: 2, + }, + { + resourceName: 'Pod', + resourceId: data[1].resource.id, + resourceSubType: data[1].resource.sub_type, + findingsCount: 1, + }, + { + resourceName: 'kubelet', + resourceId: data[0].resource.id, + resourceSubType: data[0].resource.sub_type, + findingsCount: 1, + }, + ]; - await asyncForEach(resourceOrder, async (resourceName, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.be(resourceName); - }); + await asyncForEach( + resourceOrder, + async ({ resourceName, resourceId, resourceSubType, findingsCount }, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.contain(resourceName); + expect(await groupName.getVisibleText()).to.contain(resourceId); + expect(await groupName.getVisibleText()).to.contain(resourceSubType); + expect(await groupName.getVisibleText()).to.contain(`Findings : ${findingsCount}`); + } + ); const groupCount = await grouping.getGroupCount(); expect(groupCount).to.be('3 groups'); @@ -177,16 +202,37 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(unitCount).to.be('4 findings'); const ruleNameOrder = [ - 'Another upper case rule name', - 'Upper case rule name', - 'lower case rule name', - 'some lower case rule name', + { + ruleName: 'Another upper case rule name', + findingsCount: 1, + benchmarkName: data[1].rule.benchmark.name, + }, + { + ruleName: 'Upper case rule name', + findingsCount: 1, + benchmarkName: data[0].rule.benchmark.name, + }, + { + ruleName: 'lower case rule name', + findingsCount: 1, + benchmarkName: data[2].rule.benchmark.name, + }, + { + ruleName: 'some lower case rule name', + findingsCount: 1, + benchmarkName: data[3].rule.benchmark.name, + }, ]; - await asyncForEach(ruleNameOrder, async (resourceName, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.be(resourceName); - }); + await asyncForEach( + ruleNameOrder, + async ({ ruleName, benchmarkName, findingsCount }, index) => { + const groupName = await grouping.getRowAtIndex(index); + expect(await groupName.getVisibleText()).to.contain(ruleName); + expect(await groupName.getVisibleText()).to.contain(benchmarkName); + expect(await groupName.getVisibleText()).to.contain(`Findings : ${findingsCount}`); + } + ); }); it('groups findings by cloud account and sort case sensitive asc', async () => { const groupSelector = await findings.groupSelector(); diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 9da8cbbeeed54..2c01f789d441c 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -10,13 +10,13 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { - loadTestFile(require.resolve('./findings_onboarding')); - loadTestFile(require.resolve('./findings')); + // loadTestFile(require.resolve('./findings_onboarding')); + // loadTestFile(require.resolve('./findings')); loadTestFile(require.resolve('./findings_grouping')); - loadTestFile(require.resolve('./findings_alerts')); - loadTestFile(require.resolve('./compliance_dashboard')); - loadTestFile(require.resolve('./vulnerability_dashboard')); - loadTestFile(require.resolve('./cis_integration')); - loadTestFile(require.resolve('./findings_old_data')); + // loadTestFile(require.resolve('./findings_alerts')); + // loadTestFile(require.resolve('./compliance_dashboard')); + // loadTestFile(require.resolve('./vulnerability_dashboard')); + // loadTestFile(require.resolve('./cis_integration')); + // loadTestFile(require.resolve('./findings_old_data')); }); } From bab79e7e3cd74ff6f8cdfe115c3cfe4012325cc4 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 30 Nov 2023 08:58:17 -0800 Subject: [PATCH 53/63] updating empty state component --- .../cloud_security_grouping.tsx | 7 ------- .../latest_findings_container.tsx | 17 +++++++++++++++-- .../latest_findings_group_renderer.tsx | 14 +++++++++++++- .../use_latest_findings_grouping.tsx | 2 +- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx index 30a5ba20c37ba..9a6a707f360f2 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx @@ -9,7 +9,6 @@ import { ParsedGroupingAggregation } from '@kbn/securitysolution-grouping/src'; import { Filter } from '@kbn/es-query'; import React from 'react'; import { css } from '@emotion/react'; -import { EmptyState } from '../empty_state'; import { CSP_GROUPING } from '../test_subjects'; interface CloudSecurityGroupingProps { @@ -22,7 +21,6 @@ interface CloudSecurityGroupingProps { onChangeGroupsItemsPerPage: (size: number) => void; onChangeGroupsPage: (index: number) => void; selectedGroup: string; - onResetFilters: () => void; } export const CloudSecurityGrouping = ({ @@ -35,12 +33,7 @@ export const CloudSecurityGrouping = ({ onChangeGroupsItemsPerPage, onChangeGroupsPage, selectedGroup, - onResetFilters, }: CloudSecurityGroupingProps) => { - if (!isFetching && !data.unitsCount?.value) { - return ; - } - return (
{ ); } + if (!isFetching && isFindingsRootGroupingAggregation(groupData) && !groupData.unitsCount?.value) { + return ( + <> + + + + + ); + } + if (isGroupSelected) { return ( <> @@ -86,7 +100,6 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { isFetching={isFetching} pageSize={pageSize} selectedGroup={selectedGroup} - onResetFilters={onResetFilters} /> )}
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index 235dadb98a779..09872415c76a0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -9,6 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, + EuiSkeletonTitle, EuiText, EuiTextBlockTruncate, EuiToolTip, @@ -94,8 +95,19 @@ const NullGroupComponent = ({ export const groupPanelRenderer: GroupPanelRenderer = ( selectedGroup, bucket, - nullGroupMessage + nullGroupMessage, + isLoading ) => { + if (isLoading) { + return ( + + + + ); + } const benchmarkId = firstNonNullValue(bucket.benchmarkId?.buckets?.[0]?.key); switch (selectedGroup) { case GROUPING_OPTIONS.RESOURCE: diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index a60d96694e395..1129328ca531b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -122,7 +122,7 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { /** * Type Guard for checking if the given source is a FindingsRootGroupingAggregation */ -const isFindingsRootGroupingAggregation = ( +export const isFindingsRootGroupingAggregation = ( groupData: Record | undefined ): groupData is FindingsRootGroupingAggregation => { return ( From cac60a11f0c76ad9caf6d8861c5b00f543590a0d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 30 Nov 2023 14:22:34 -0800 Subject: [PATCH 54/63] adding more tests subjects --- .../public/components/compliance_score_bar.tsx | 2 ++ .../cloud_security_posture/public/components/test_subjects.ts | 1 + .../latest_findings/latest_findings_group_renderer.tsx | 2 ++ .../public/pages/configurations/test_subjects.ts | 1 + 4 files changed, 6 insertions(+) diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx index cc155b4a42aba..d71d6c2e2384b 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { calculatePostureScore } from '../../common/utils/helpers'; import { statusColors } from '../common/constants'; +import { CSP_FINDINGS_COMPLIANCE_SCORE } from './test_subjects'; /** * This component will take 100% of the width set by the parent @@ -85,6 +86,7 @@ export const ComplianceScoreBar = ({ > {getAbbreviatedNumber(bucket.doc_count)} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/test_subjects.ts index b0d5a8ffd19f5..b465b58f45887 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/test_subjects.ts @@ -20,6 +20,7 @@ export const LATEST_FINDINGS_CONTAINER = 'latest_findings_container'; export const LATEST_FINDINGS_TABLE = 'latest_findings_table'; export const FINDINGS_GROUP_BY_SELECTOR = 'findings_group_by_selector'; +export const FINDINGS_GROUPING_COUNTER = 'findings_grouping_counter'; export const getFindingsTableRowTestId = (id: string) => `findings_table_row_${id}`; export const getFindingsTableCellTestId = (columnId: string, rowId: string) => From bab7688999a82592ca60b9506b54b748aeec1146 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 30 Nov 2023 14:27:06 -0800 Subject: [PATCH 55/63] updating FTR tests --- .../pages/findings_grouping.ts | 241 ++++++++++++------ 1 file changed, 162 insertions(+), 79 deletions(-) diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts index 6d432f686a1eb..2939f3eed9266 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_grouping.ts @@ -17,13 +17,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'findings', 'header']); const chance = new Chance(); + const cspmResourceId = chance.guid(); + const cspmResourceName = 'gcp-resource'; + const cspmResourceSubType = 'gcp-monitoring'; + // We need to use a dataset for the tests to run // We intentionally make some fields start with a capital letter to test that the query bar is case-insensitive/case-sensitive const data = [ { '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, - result: { evaluation: 'passed' }, + result: { evaluation: 'failed' }, orchestrator: { cluster: { id: '1', @@ -41,16 +45,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, type: 'process', }, - cluster_id: 'Upper case cluster id', }, { '@timestamp': new Date().toISOString(), resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, result: { evaluation: 'passed' }, - cloud: { - account: { + orchestrator: { + cluster: { id: '1', - name: 'Account 1', + name: 'Cluster 2', }, }, rule: { @@ -64,11 +67,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, type: 'process', }, - cluster_id: 'Another Upper case cluster id', }, { '@timestamp': new Date().toISOString(), - resource: { id: chance.guid(), name: `process`, sub_type: 'another lower case type' }, + resource: { id: cspmResourceId, name: cspmResourceName, sub_type: cspmResourceSubType }, result: { evaluation: 'passed' }, cloud: { account: { @@ -80,18 +82,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { name: 'Another upper case rule name', section: 'lower case section', benchmark: { - id: 'cis_k8s', - posture_type: 'kspm', - name: 'CIS Kubernetes V1.23', - version: 'v1.0.0', + id: 'cis_gcp', + posture_type: 'cspm', + name: 'CIS Google Cloud Platform Foundation', + version: 'v2.0.0', }, type: 'process', }, - cluster_id: 'lower case cluster id', }, { '@timestamp': new Date().toISOString(), - resource: { id: chance.guid(), name: `process`, sub_type: 'Upper case type again' }, + resource: { id: cspmResourceId, name: cspmResourceName, sub_type: cspmResourceSubType }, result: { evaluation: 'failed' }, cloud: { account: { @@ -103,14 +104,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { name: 'some lower case rule name', section: 'another lower case section', benchmark: { - id: 'cis_k8s', - posture_type: 'kspm', - name: 'CIS Kubernetes V1.23', - version: 'v1.0.0', + id: 'cis_gcp', + posture_type: 'cspm', + name: 'CIS Google Cloud Platform Foundation', + version: 'v2.0.0', }, type: 'process', }, - cluster_id: 'another lower case cluster id', }, ]; @@ -143,7 +143,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('Default Grouping', async () => { - it('groups findings by resource and sort by failed findings desc', async () => { + it('groups findings by resource and sort by compliance score desc', async () => { const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Resource'); @@ -152,33 +152,46 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const resourceOrder = [ { - resourceName: 'process', - resourceId: data[2].resource.id, - resourceSubType: data[2].resource.sub_type, - findingsCount: 2, + resourceName: 'kubelet', + resourceId: data[0].resource.id, + resourceSubType: data[0].resource.sub_type, + findingsCount: '1', + complianceScore: '0%', + }, + { + resourceName: cspmResourceName, + resourceId: cspmResourceId, + resourceSubType: cspmResourceSubType, + findingsCount: '2', + complianceScore: '50%', }, { resourceName: 'Pod', resourceId: data[1].resource.id, resourceSubType: data[1].resource.sub_type, - findingsCount: 1, - }, - { - resourceName: 'kubelet', - resourceId: data[0].resource.id, - resourceSubType: data[0].resource.sub_type, - findingsCount: 1, + findingsCount: '1', + complianceScore: '100%', }, ]; await asyncForEach( resourceOrder, - async ({ resourceName, resourceId, resourceSubType, findingsCount }, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.contain(resourceName); - expect(await groupName.getVisibleText()).to.contain(resourceId); - expect(await groupName.getVisibleText()).to.contain(resourceSubType); - expect(await groupName.getVisibleText()).to.contain(`Findings : ${findingsCount}`); + async ( + { resourceName, resourceId, resourceSubType, findingsCount, complianceScore }, + index + ) => { + const groupRow = await grouping.getRowAtIndex(index); + expect(await groupRow.getVisibleText()).to.contain(resourceName); + expect(await groupRow.getVisibleText()).to.contain(resourceId); + expect(await groupRow.getVisibleText()).to.contain(resourceSubType); + expect( + await ( + await groupRow.findByTestSubject('cloudSecurityFindingsComplianceScore') + ).getVisibleText() + ).to.be(complianceScore); + expect( + await (await groupRow.findByTestSubject('findings_grouping_counter')).getVisibleText() + ).to.be(findingsCount); } ); @@ -188,7 +201,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const unitCount = await grouping.getUnitCount(); expect(unitCount).to.be('4 findings'); }); - it('groups findings by rule name and sort case sensitive asc', async () => { + it('groups findings by rule name and sort by compliance score desc', async () => { const groupSelector = await findings.groupSelector(); await groupSelector.openDropDown(); await groupSelector.setValue('Rule name'); @@ -202,39 +215,50 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(unitCount).to.be('4 findings'); const ruleNameOrder = [ - { - ruleName: 'Another upper case rule name', - findingsCount: 1, - benchmarkName: data[1].rule.benchmark.name, - }, { ruleName: 'Upper case rule name', - findingsCount: 1, + findingsCount: '1', + complianceScore: '0%', benchmarkName: data[0].rule.benchmark.name, }, { - ruleName: 'lower case rule name', - findingsCount: 1, + ruleName: 'some lower case rule name', + findingsCount: '1', + complianceScore: '0%', + benchmarkName: data[3].rule.benchmark.name, + }, + { + ruleName: 'Another upper case rule name', + findingsCount: '1', + complianceScore: '100%', benchmarkName: data[2].rule.benchmark.name, }, { - ruleName: 'some lower case rule name', - findingsCount: 1, - benchmarkName: data[3].rule.benchmark.name, + ruleName: 'lower case rule name', + findingsCount: '1', + complianceScore: '100%', + benchmarkName: data[1].rule.benchmark.name, }, ]; await asyncForEach( ruleNameOrder, - async ({ ruleName, benchmarkName, findingsCount }, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.contain(ruleName); - expect(await groupName.getVisibleText()).to.contain(benchmarkName); - expect(await groupName.getVisibleText()).to.contain(`Findings : ${findingsCount}`); + async ({ ruleName, benchmarkName, findingsCount, complianceScore }, index) => { + const groupRow = await grouping.getRowAtIndex(index); + expect(await groupRow.getVisibleText()).to.contain(ruleName); + expect(await groupRow.getVisibleText()).to.contain(benchmarkName); + expect( + await ( + await groupRow.findByTestSubject('cloudSecurityFindingsComplianceScore') + ).getVisibleText() + ).to.be(complianceScore); + expect( + await (await groupRow.findByTestSubject('findings_grouping_counter')).getVisibleText() + ).to.be(findingsCount); } ); }); - it('groups findings by cloud account and sort case sensitive asc', async () => { + it('groups findings by cloud account and sort by compliance score desc', async () => { const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Cloud account'); @@ -247,31 +271,98 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const unitCount = await grouping.getUnitCount(); expect(unitCount).to.be('4 findings'); - const cloudNameOrder = ['Account 1', 'Account 2', '—']; + const cloudNameOrder = [ + { + cloudName: 'Account 2', + findingsCount: '1', + complianceScore: '0%', + benchmarkName: data[3].rule.benchmark.name, + }, + { + cloudName: 'Account 1', + findingsCount: '1', + complianceScore: '100%', + benchmarkName: data[2].rule.benchmark.name, + }, + { + cloudName: 'No cloud account', + findingsCount: '2', + complianceScore: '50%', + benchmarkName: data[0].rule.benchmark.name, + }, + ]; - await asyncForEach(cloudNameOrder, async (resourceName, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.be(resourceName); - }); + await asyncForEach( + cloudNameOrder, + async ({ cloudName, complianceScore, findingsCount, benchmarkName }, index) => { + const groupRow = await grouping.getRowAtIndex(index); + expect(await groupRow.getVisibleText()).to.contain(cloudName); + + if (cloudName !== 'No cloud account') { + expect(await groupRow.getVisibleText()).to.contain(benchmarkName); + } + + expect( + await ( + await groupRow.findByTestSubject('cloudSecurityFindingsComplianceScore') + ).getVisibleText() + ).to.be(complianceScore); + expect( + await (await groupRow.findByTestSubject('findings_grouping_counter')).getVisibleText() + ).to.be(findingsCount); + } + ); }); - it('groups findings by Kubernetes cluster and sort case sensitive asc', async () => { + it('groups findings by Kubernetes cluster and sort by compliance score desc', async () => { const groupSelector = await findings.groupSelector(); await groupSelector.setValue('Kubernetes cluster'); const grouping = await findings.findingsGrouping(); const groupCount = await grouping.getGroupCount(); - expect(groupCount).to.be('2 groups'); + expect(groupCount).to.be('3 groups'); const unitCount = await grouping.getUnitCount(); expect(unitCount).to.be('4 findings'); - const cloudNameOrder = ['Cluster 1', '—']; + const kubernetesOrder = [ + { + clusterName: 'Cluster 1', + findingsCount: '1', + complianceScore: '0%', + benchmarkName: data[0].rule.benchmark.name, + }, + { + clusterName: 'Cluster 2', + findingsCount: '1', + complianceScore: '100%', + benchmarkName: data[1].rule.benchmark.name, + }, + { + clusterName: 'No Kubernetes cluster', + findingsCount: '2', + complianceScore: '50%', + }, + ]; - await asyncForEach(cloudNameOrder, async (resourceName, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.be(resourceName); - }); + await asyncForEach( + kubernetesOrder, + async ({ clusterName, complianceScore, findingsCount, benchmarkName }, index) => { + const groupRow = await grouping.getRowAtIndex(index); + expect(await groupRow.getVisibleText()).to.contain(clusterName); + if (clusterName !== 'No Kubernetes cluster') { + expect(await groupRow.getVisibleText()).to.contain(benchmarkName); + } + expect( + await ( + await groupRow.findByTestSubject('cloudSecurityFindingsComplianceScore') + ).getVisibleText() + ).to.be(complianceScore); + expect( + await (await groupRow.findByTestSubject('findings_grouping_counter')).getVisibleText() + ).to.be(findingsCount); + } + ); }); }); describe('SearchBar', () => { @@ -285,12 +376,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const grouping = await findings.findingsGrouping(); - const resourceOrder = ['kubelet']; - - await asyncForEach(resourceOrder, async (resourceName, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.be(resourceName); - }); + const groupRow = await grouping.getRowAtIndex(0); + expect(await groupRow.getVisibleText()).to.contain(data[0].resource.name); const groupCount = await grouping.getGroupCount(); expect(groupCount).to.be('1 group'); @@ -318,12 +405,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const grouping = await findings.findingsGrouping(); - const resourceOrder = ['kubelet']; - - await asyncForEach(resourceOrder, async (resourceName, index) => { - const groupName = await grouping.getRowAtIndex(index); - expect(await groupName.getVisibleText()).to.be(resourceName); - }); + const groupRow = await grouping.getRowAtIndex(0); + expect(await groupRow.getVisibleText()).to.contain(data[0].resource.name); const groupCount = await grouping.getGroupCount(); expect(groupCount).to.be('1 group'); @@ -346,7 +429,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await (await firstRow.findByCssSelector('button')).click(); const latestFindingsTable = findings.createDataTableObject('latest_findings_table'); expect(await latestFindingsTable.getRowsCount()).to.be(1); - expect(await latestFindingsTable.hasColumnValue('rule.name', 'lower case rule name')).to.be( + expect(await latestFindingsTable.hasColumnValue('rule.name', data[0].rule.name)).to.be( true ); }); From c5e5a9c9adb8539a682bc374d90054104bd9786b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Thu, 30 Nov 2023 14:41:26 -0800 Subject: [PATCH 56/63] fix ci checks, split utils hooks --- .../use_cloud_posture_data_table/index.ts | 2 + .../use_base_es_query.ts | 81 ++++++++++++++++ .../use_cloud_posture_data_table.ts | 15 ++- .../use_persisted_query.ts | 28 ++++++ .../use_cloud_posture_data_table/utils.ts | 96 ------------------- .../use_cloud_security_grouping.ts | 6 +- .../latest_findings/constants.ts | 2 +- 7 files changed, 127 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts create mode 100644 x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_persisted_query.ts diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts index 60a917846dc99..b026744f008bd 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/index.ts @@ -6,3 +6,5 @@ */ export * from './use_cloud_posture_data_table'; +export * from './use_base_es_query'; +export * from './use_persisted_query'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts new file mode 100644 index 0000000000000..4adffa100e48c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts @@ -0,0 +1,81 @@ +/* + * 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 { buildEsQuery, EsQueryConfig } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { useEffect, useMemo } from 'react'; +import { FindingsBaseESQueryConfig, FindingsBaseProps, FindingsBaseURLQuery } from '../../types'; +import { useKibana } from '../use_kibana'; + +const getBaseQuery = ({ + dataView, + query, + filters, + config, +}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { + try { + return { + query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query + }; + } catch (error) { + return { + query: undefined, + error: error instanceof Error ? error : new Error('Unknown Error'), + }; + } +}; + +export const useBaseEsQuery = ({ + dataView, + filters = [], + query, + nonPersistedFilters, +}: FindingsBaseURLQuery & FindingsBaseProps) => { + const { + notifications: { toasts }, + data: { + query: { filterManager, queryString }, + }, + uiSettings, + } = useKibana().services; + const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards'); + const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]); + const baseEsQuery = useMemo( + () => + getBaseQuery({ + dataView, + filters: filters.concat(nonPersistedFilters ?? []).flat(), + query, + config, + }), + [dataView, filters, nonPersistedFilters, query, config] + ); + + /** + * Sync filters with the URL query + */ + useEffect(() => { + filterManager.setAppFilters(filters); + queryString.setQuery(query); + }, [filters, filterManager, queryString, query]); + + const handleMalformedQueryError = () => { + const error = baseEsQuery instanceof Error ? baseEsQuery : undefined; + if (error) { + toasts.addError(error, { + title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { + defaultMessage: 'Query Error', + }), + toastLifeTimeMs: 1000 * 5, + }); + } + }; + + useEffect(handleMalformedQueryError, [baseEsQuery, toasts]); + + return baseEsQuery; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts index b7b928a208c0a..ae21f45c7a4e8 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts @@ -11,9 +11,11 @@ import { CriteriaWithPagination } from '@elastic/eui'; import { DataTableRecord } from '@kbn/discover-utils/types'; import { useUrlQuery } from '../use_url_query'; import { usePageSize } from '../use_page_size'; -import { getDefaultQuery, useBaseEsQuery, usePersistedQuery } from './utils'; +import { getDefaultQuery } from './utils'; import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants'; import { FindingsBaseURLQuery } from '../../types'; +import { useBaseEsQuery } from './use_base_es_query'; +import { usePersistedQuery } from './use_persisted_query'; type URLQuery = FindingsBaseURLQuery & Record; @@ -140,7 +142,16 @@ export const useCloudPostureDataTable = ({ setUrlQuery, sort: urlQuery.sort, filters: urlQuery.filters, - query: baseEsQuery.query, + query: baseEsQuery.query + ? baseEsQuery.query + : { + bool: { + must: [], + filter: [], + should: [], + must_not: [], + }, + }, queryError, pageIndex: urlQuery.pageIndex, urlQuery, diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_persisted_query.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_persisted_query.ts new file mode 100644 index 0000000000000..c3731c0139ce3 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_persisted_query.ts @@ -0,0 +1,28 @@ +/* + * 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 { useCallback } from 'react'; +import { type Query } from '@kbn/es-query'; +import { FindingsBaseURLQuery } from '../../types'; +import { useKibana } from '../use_kibana'; + +export const usePersistedQuery = (getter: ({ filters, query }: FindingsBaseURLQuery) => T) => { + const { + data: { + query: { filterManager, queryString }, + }, + } = useKibana().services; + + return useCallback( + () => + getter({ + filters: filterManager.getAppFilters(), + query: queryString.getQuery() as Query, + }), + [getter, filterManager, queryString] + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts index 576c15aa18e7c..c715b6a90b4ca 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/utils.ts @@ -5,35 +5,7 @@ * 2.0. */ -import { useEffect, useCallback, useMemo } from 'react'; -import { buildEsQuery, EsQueryConfig } from '@kbn/es-query'; import type { EuiBasicTableProps, Pagination } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { type Query } from '@kbn/es-query'; -import { useKibana } from '../use_kibana'; -import type { - FindingsBaseESQueryConfig, - FindingsBaseProps, - FindingsBaseURLQuery, -} from '../../types'; - -const getBaseQuery = ({ - dataView, - query, - filters, - config, -}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { - try { - return { - query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query - }; - } catch (error) { - return { - query: undefined, - error: error instanceof Error ? error : new Error('Unknown Error'), - }; - } -}; type TablePagination = NonNullable['pagination']>; @@ -55,74 +27,6 @@ export const getPaginationQuery = ({ size: pageSize, }); -export const useBaseEsQuery = ({ - dataView, - filters = [], - query, - nonPersistedFilters, -}: FindingsBaseURLQuery & FindingsBaseProps) => { - const { - notifications: { toasts }, - data: { - query: { filterManager, queryString }, - }, - uiSettings, - } = useKibana().services; - const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards'); - const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]); - const baseEsQuery = useMemo( - () => - getBaseQuery({ - dataView, - filters: filters.concat(nonPersistedFilters ?? []).flat(), - query, - config, - }), - [dataView, filters, nonPersistedFilters, query, config] - ); - - /** - * Sync filters with the URL query - */ - useEffect(() => { - filterManager.setAppFilters(filters); - queryString.setQuery(query); - }, [filters, filterManager, queryString, query]); - - const handleMalformedQueryError = () => { - const error = baseEsQuery instanceof Error ? baseEsQuery : undefined; - if (error) { - toasts.addError(error, { - title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { - defaultMessage: 'Query Error', - }), - toastLifeTimeMs: 1000 * 5, - }); - } - }; - - useEffect(handleMalformedQueryError, [baseEsQuery, toasts]); - - return baseEsQuery; -}; - -export const usePersistedQuery = (getter: ({ filters, query }: FindingsBaseURLQuery) => T) => { - const { - data: { - query: { filterManager, queryString }, - }, - } = useKibana().services; - - return useCallback( - () => - getter({ - filters: filterManager.getAppFilters(), - query: queryString.getQuery() as Query, - }), - [getter, filterManager, queryString] - ); -}; - export const getDefaultQuery = ({ query, filters }: any): any => ({ query, filters, diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index e3a329a6678fc..c59d382144524 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -14,11 +14,9 @@ import { GroupStatsRenderer, } from '@kbn/securitysolution-grouping/src'; import { useUrlQuery } from '../../common/hooks/use_url_query'; -import { - useBaseEsQuery, - usePersistedQuery, -} from '../../common/hooks/use_cloud_posture_data_table/utils'; + import { FindingsBaseURLQuery } from '../../common/types'; +import { useBaseEsQuery, usePersistedQuery } from '../../common/hooks/use_cloud_posture_data_table'; const DEFAULT_PAGE_SIZE = 10; const GROUPING_ID = 'cspLatestFindings'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index fcabc99fed089..0e936aff686d7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -40,7 +40,7 @@ export const NULL_GROUPING_MESSAGES = { KUBERNETES: i18n.translate('xpack.csp.findings.grouping.kubernetes.nullGroupTitle', { defaultMessage: 'No Kubernetes cluster', }), - GENERIC_MESSAGE: i18n.translate('xpack.csp.findings.grouping.kubernetes.nullGroupTitle', { + GENERIC_MESSAGE: i18n.translate('xpack.csp.findings.grouping.default.nullGroupTitle', { defaultMessage: 'No grouping', }), }; From 9ef7cd2088f84f34004bded1161d2f31c0099a72 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 10:42:31 -0800 Subject: [PATCH 57/63] unskipping FTR tests --- .../pages/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 2c01f789d441c..9da8cbbeeed54 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -10,13 +10,13 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { - // loadTestFile(require.resolve('./findings_onboarding')); - // loadTestFile(require.resolve('./findings')); + loadTestFile(require.resolve('./findings_onboarding')); + loadTestFile(require.resolve('./findings')); loadTestFile(require.resolve('./findings_grouping')); - // loadTestFile(require.resolve('./findings_alerts')); - // loadTestFile(require.resolve('./compliance_dashboard')); - // loadTestFile(require.resolve('./vulnerability_dashboard')); - // loadTestFile(require.resolve('./cis_integration')); - // loadTestFile(require.resolve('./findings_old_data')); + loadTestFile(require.resolve('./findings_alerts')); + loadTestFile(require.resolve('./compliance_dashboard')); + loadTestFile(require.resolve('./vulnerability_dashboard')); + loadTestFile(require.resolve('./cis_integration')); + loadTestFile(require.resolve('./findings_old_data')); }); } From 93107f703f269a7a1da5bf309b2ded3af8a762a7 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 11:15:17 -0800 Subject: [PATCH 58/63] adding tests for get_abbreviated_number --- .../utils/get_abbreviated_number.test.ts | 35 +++++++++++++++++++ .../common/utils/get_abbreviated_number.ts | 3 ++ .../layout/findings_distribution_bar.tsx | 8 ++--- 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.test.ts diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.test.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.test.ts new file mode 100644 index 0000000000000..23cf2512ad2cf --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { getAbbreviatedNumber } from './get_abbreviated_number'; + +describe('getAbbreviatedNumber', () => { + it('should return the same value if it is less than 1000', () => { + expect(getAbbreviatedNumber(0)).toBe(0); + expect(getAbbreviatedNumber(1)).toBe(1); + expect(getAbbreviatedNumber(500)).toBe(500); + expect(getAbbreviatedNumber(999)).toBe(999); + }); + + it('should use numeral to format the value if it is greater than or equal to 1000', () => { + expect(getAbbreviatedNumber(1000)).toBe('1.0k'); + + expect(getAbbreviatedNumber(1200)).toBe('1.2k'); + + expect(getAbbreviatedNumber(3500000)).toBe('3.5m'); + + expect(getAbbreviatedNumber(2800000000)).toBe('2.8b'); + + expect(getAbbreviatedNumber(5900000000000)).toBe('5.9t'); + + expect(getAbbreviatedNumber(59000000000000000)).toBe('59000.0t'); + }); + + it('should return 0 if the value is NaN', () => { + expect(getAbbreviatedNumber(NaN)).toBe(0); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts index 4bdb3525747ee..8db05337f5ddf 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts @@ -14,5 +14,8 @@ import numeral from '@elastic/numeral'; trillion: 't' */ export const getAbbreviatedNumber = (value: number) => { + if (isNaN(value)) { + return 0; + } return value < 1000 ? value : numeral(value).format('0.0a'); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx index 1f1241c4d1dfa..1cf084220ea29 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import numeral from '@elastic/numeral'; +import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_number'; import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants'; import { statusColors } from '../../../common/constants'; import type { Evaluation } from '../../../../common/types'; @@ -28,8 +28,6 @@ interface Props { distributionOnClick: (evaluation: Evaluation) => void; } -const formatNumber = (value: number) => (value < 1000 ? value : numeral(value).format('0.0a')); - export const CurrentPageOfTotal = ({ pageEnd, pageStart, @@ -48,7 +46,7 @@ export const CurrentPageOfTotal = ({ values={{ pageStart: {pageStart}, pageEnd: {pageEnd}, - total: {formatNumber(total)}, + total: {getAbbreviatedNumber(total)}, type, }} /> @@ -164,7 +162,7 @@ const Counter = ({ label, value, color }: { label: string; value: number; color: {label}
- {formatNumber(value)} + {getAbbreviatedNumber(value)}
); From 97d30ee7d87a30942c79683a8e64f0279a768f36 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 11:50:11 -0800 Subject: [PATCH 59/63] updating constants to reflect field name --- .../latest_findings/constants.ts | 12 +++++------ .../latest_findings_group_renderer.tsx | 6 +++--- .../use_latest_findings_grouping.tsx | 20 +++---------------- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index 0e936aff686d7..7f3d49aab7e30 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -17,10 +17,10 @@ export const FINDINGS_UNIT = (totalCount: number) => }); export const GROUPING_OPTIONS = { - RESOURCE: 'resource.name', + RESOURCE_NAME: 'resource.name', RULE_NAME: 'rule.name', - CLOUD_ACCOUNT: 'cloud.account.name', - KUBERNETES: 'orchestrator.cluster.name', + CLOUD_ACCOUNT_NAME: 'cloud.account.name', + ORCHESTRATOR_CLUSTER_NAME: 'orchestrator.cluster.name', }; export const NULL_GROUPING_UNIT = i18n.translate('xpack.csp.findings.grouping.nullGroupUnit', { @@ -50,7 +50,7 @@ export const defaultGroupingOptions: GroupOption[] = [ label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { defaultMessage: 'Resource', }), - key: GROUPING_OPTIONS.RESOURCE, + key: GROUPING_OPTIONS.RESOURCE_NAME, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { @@ -62,13 +62,13 @@ export const defaultGroupingOptions: GroupOption[] = [ label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { defaultMessage: 'Cloud account', }), - key: GROUPING_OPTIONS.CLOUD_ACCOUNT, + key: GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', { defaultMessage: 'Kubernetes cluster', }), - key: GROUPING_OPTIONS.KUBERNETES, + key: GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME, }, ]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index 4ee879e7bc131..88f0bfaf71396 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -111,7 +111,7 @@ export const groupPanelRenderer: GroupPanelRenderer } const benchmarkId = firstNonNullValue(bucket.benchmarkId?.buckets?.[0]?.key); switch (selectedGroup) { - case GROUPING_OPTIONS.RESOURCE: + case GROUPING_OPTIONS.RESOURCE_NAME: return nullGroupMessage ? ( ) : ( @@ -166,7 +166,7 @@ export const groupPanelRenderer: GroupPanelRenderer
); - case GROUPING_OPTIONS.CLOUD_ACCOUNT: + case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: return nullGroupMessage ? ( ) : ( @@ -195,7 +195,7 @@ export const groupPanelRenderer: GroupPanelRenderer
); - case GROUPING_OPTIONS.KUBERNETES: + case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: return nullGroupMessage ? ( ) : ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 1129328ca531b..b83485f1cb469 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -76,45 +76,31 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { ]; switch (field) { - case GROUPING_OPTIONS.RESOURCE: + case GROUPING_OPTIONS.RESOURCE_NAME: return [ ...aggMetrics, getTermAggregation('resourceName', 'resource.id'), getTermAggregation('resourceSubType', 'resource.sub_type'), getTermAggregation('resourceType', 'resource.type'), ]; - case GROUPING_OPTIONS.RULE_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), getTermAggregation('benchmarkVersion', 'rule.benchmark.version'), ]; - case GROUPING_OPTIONS.CLOUD_ACCOUNT: + case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), getTermAggregation('benchmarkId', 'rule.benchmark.id'), ]; - case GROUPING_OPTIONS.KUBERNETES: + case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), getTermAggregation('benchmarkId', 'rule.benchmark.id'), ]; - case 'resource.type': - return [ - ...aggMetrics, - getTermAggregation('resourceName', 'resource.id'), - getTermAggregation('resourceType', 'resource.type'), - ]; - case 'resource.sub_type': - return [ - ...aggMetrics, - getTermAggregation('resourceName', 'resource.id'), - getTermAggregation('resourceType', 'resource.type'), - getTermAggregation('resourceSubType', 'resource.sub_type'), - ]; } return aggMetrics; }; From 236a95dcb48890b7790baeb9d5a94bbc6b83e417 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 11:59:23 -0800 Subject: [PATCH 60/63] added deprecation notice --- .../findings_by_resource_container.tsx | 6 ++++++ .../findings_by_resource_table.tsx | 16 ++++++++++++++++ .../resource_findings_container.tsx | 3 +++ .../resource_findings_table.tsx | 3 +++ .../resource_findings/use_resource_findings.ts | 3 +++ .../use_findings_by_resource.ts | 6 ++++++ 6 files changed, 37 insertions(+) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx index 7f483c3ee0847..3054ad352a5bd 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx @@ -39,6 +39,9 @@ const getDefaultQuery = ({ sort: { field: 'compliance_score' as keyof CspFinding, direction: 'asc' }, }); +/** + * @deprecated: This component is deprecated and will be removed in the next release. + */ export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) => ( ); +/** + * @deprecated: This component is deprecated and will be removed in the next release. + */ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { const { queryError, query, pageSize, setTableOptions, urlQuery, setUrlQuery, onResetFilters } = useCloudPostureTable({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx index ce3f55e03417d..71c4219d3b852 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx @@ -29,6 +29,10 @@ import { } from '../layout/findings_layout'; import { EmptyState } from '../../../components/empty_state'; +/** + * @deprecated: This function is deprecated and will be removed in the next release. + * use getAbbreviatedNumber from x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts + */ export const formatNumber = (value: number) => value < 1000 ? value : numeral(value).format('0.0a'); @@ -44,11 +48,17 @@ interface Props { onResetFilters: () => void; } +/** + * @deprecated: This function is deprecated and will be removed in the next release. + */ export const getResourceId = (resource: FindingsByResourcePage) => { const sections = resource['rule.section'] || []; return [resource.resource_id, ...sections].join('/'); }; +/** + * @deprecated: This component is deprecated and will be removed in the next release. + */ const FindingsByResourceTableComponent = ({ items, loading, @@ -189,8 +199,14 @@ const baseColumns: Array> = type BaseFindingColumnName = typeof baseColumns[number]['field']; +/** + * @deprecated: This function is deprecated and will be removed in the next release. + */ export const findingsByResourceColumns = Object.fromEntries( baseColumns.map((column) => [column.field, column]) ) as Record; +/** + * @deprecated: This component is deprecated and will be removed in the next release. + */ export const FindingsByResourceTable = React.memo(FindingsByResourceTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx index 83182d66665d7..f606241862ee2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx @@ -95,6 +95,9 @@ const getResourceFindingSharedValues = (sharedValues: { }, ]; +/** + * @deprecated: This component is deprecated and will be removed in the next release. + */ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { const params = useParams<{ resourceId: string }>(); const decodedResourceId = decodeURIComponent(params.resourceId); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx index 09f046f504ea7..4dd7070af88f1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx @@ -106,4 +106,7 @@ const ResourceFindingsTableComponent = ({ ); }; +/** + * @deprecated: This component is deprecated and will be removed in the next release. + */ export const ResourceFindingsTable = React.memo(ResourceFindingsTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts index cd1a3484548c7..46a5e12665660 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts @@ -80,6 +80,9 @@ const getResourceFindingsQuery = ({ ignore_unavailable: false, }); +/** + * @deprecated: This hook is deprecated and will be removed in the next release. + */ export const useResourceFindings = (options: UseResourceFindingsOptions) => { const { data, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts index d0253966bc87c..e4bbd955f6092 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts @@ -76,6 +76,9 @@ interface FindingsAggBucket extends AggregationsStringRareTermsBucketKeys { cis_sections: AggregationsMultiBucketAggregateBase; } +/** + * @deprecated: This hook is deprecated and will be removed in the next release. + */ export const getFindingsByResourceAggQuery = ({ query, sortDirection, @@ -168,6 +171,9 @@ const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResour }, }); +/** + * @deprecated: This hook is deprecated and will be removed in the next release. + */ export const useFindingsByResource = (options: UseFindingsByResourceOptions) => { const { data, From 189fc5e5b845b49dcf55fd97aebd0b8e42f1493e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 12:08:07 -0800 Subject: [PATCH 61/63] update comments --- .../public/common/utils/get_abbreviated_number.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts index 8db05337f5ddf..353a6482f4706 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts @@ -6,13 +6,14 @@ */ import numeral from '@elastic/numeral'; -/* - * Retrieve a number abbreviated in the following format: - thousand: 'k' - million: 'm' - billion: 'b' - trillion: 't' - */ +/** + * Returns an abbreviated number when the value is greater than or equal to 1000. + * The abbreviated number is formatted using numeral: + * - thousand: k + * - million: m + * - billion: b + * - trillion: t + * */ export const getAbbreviatedNumber = (value: number) => { if (isNaN(value)) { return 0; From ad489ed51764f11c19bf0e734520760331525f9f Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 12:25:40 -0800 Subject: [PATCH 62/63] updating constants --- .../configurations/latest_findings/constants.ts | 13 +++++++------ .../latest_findings_group_renderer.tsx | 14 ++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index 7f3d49aab7e30..e2e4585906bae 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -28,19 +28,20 @@ export const NULL_GROUPING_UNIT = i18n.translate('xpack.csp.findings.grouping.nu }); export const NULL_GROUPING_MESSAGES = { - RESOURCE: i18n.translate('xpack.csp.findings.grouping.resource.nullGroupTitle', { + RESOURCE_NAME: i18n.translate('xpack.csp.findings.grouping.resource.nullGroupTitle', { defaultMessage: 'No resource', }), RULE_NAME: i18n.translate('xpack.csp.findings.grouping.rule.nullGroupTitle', { defaultMessage: 'No rule', }), - CLOUD_ACCOUNT: i18n.translate('xpack.csp.findings.grouping.cloudAccount.nullGroupTitle', { + CLOUD_ACCOUNT_NAME: i18n.translate('xpack.csp.findings.grouping.cloudAccount.nullGroupTitle', { defaultMessage: 'No cloud account', }), - KUBERNETES: i18n.translate('xpack.csp.findings.grouping.kubernetes.nullGroupTitle', { - defaultMessage: 'No Kubernetes cluster', - }), - GENERIC_MESSAGE: i18n.translate('xpack.csp.findings.grouping.default.nullGroupTitle', { + ORCHESTRATOR_CLUSTER_NAME: i18n.translate( + 'xpack.csp.findings.grouping.kubernetes.nullGroupTitle', + { defaultMessage: 'No Kubernetes cluster' } + ), + DEFAULT: i18n.translate('xpack.csp.findings.grouping.default.nullGroupTitle', { defaultMessage: 'No grouping', }), }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index 88f0bfaf71396..d0684452fb23a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -113,7 +113,7 @@ export const groupPanelRenderer: GroupPanelRenderer switch (selectedGroup) { case GROUPING_OPTIONS.RESOURCE_NAME: return nullGroupMessage ? ( - + ) : ( @@ -168,7 +168,10 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: return nullGroupMessage ? ( - + ) : ( {benchmarkId && ( @@ -197,7 +200,10 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: return nullGroupMessage ? ( - + ) : ( {benchmarkId && ( @@ -226,7 +232,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); default: return nullGroupMessage ? ( - + ) : ( From 4d61f3f0fea300b41c499c913c8c8b3b3a7bf887 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 1 Dec 2023 12:59:55 -0800 Subject: [PATCH 63/63] code optimization / loading state --- .../cloud_security_grouping.tsx | 36 ++++++++++++- .../public/components/test_subjects.ts | 1 + .../latest_findings_container.tsx | 50 +++++++------------ .../use_latest_findings_grouping.tsx | 4 ++ 4 files changed, 57 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx index 9a6a707f360f2..4ec3d3578a541 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/cloud_security_grouping.tsx @@ -9,7 +9,7 @@ import { ParsedGroupingAggregation } from '@kbn/securitysolution-grouping/src'; import { Filter } from '@kbn/es-query'; import React from 'react'; import { css } from '@emotion/react'; -import { CSP_GROUPING } from '../test_subjects'; +import { CSP_GROUPING, CSP_GROUPING_LOADING } from '../test_subjects'; interface CloudSecurityGroupingProps { data: ParsedGroupingAggregation; @@ -21,8 +21,38 @@ interface CloudSecurityGroupingProps { onChangeGroupsItemsPerPage: (size: number) => void; onChangeGroupsPage: (index: number) => void; selectedGroup: string; + isGroupLoading?: boolean; } +/** + * This component is used to render the loading state of the CloudSecurityGrouping component + * It's used to avoid the flickering of the table when the data is loading + */ +const CloudSecurityGroupingLoading = ({ + grouping, + pageSize, +}: Pick) => { + return ( +
+ {grouping.getGrouping({ + activePage: 0, + data: { + groupsCount: { value: 1 }, + unitsCount: { value: 1 }, + }, + groupingLevel: 0, + inspectButton: undefined, + isLoading: true, + itemsPerPage: pageSize, + renderChildComponent: () => <>, + onGroupClose: () => {}, + selectedGroup: '', + takeActionItems: () => [], + })} +
+ ); +}; + export const CloudSecurityGrouping = ({ data, renderChildComponent, @@ -33,7 +63,11 @@ export const CloudSecurityGrouping = ({ onChangeGroupsItemsPerPage, onChangeGroupsPage, selectedGroup, + isGroupLoading, }: CloudSecurityGroupingProps) => { + if (isGroupLoading) { + return ; + } return (
{ totalPassedFindings, onDistributionBarClick, totalFailedFindings, + isEmptyResults, } = useLatestFindingsGrouping({ dataView, groupPanelRenderer, groupStatsRenderer }); - if (error) { + if (error || isEmptyResults) { return ( <> - + {error && } + {isEmptyResults && } ); } - - if (!isFetching && isFindingsRootGroupingAggregation(groupData) && !groupData.unitsCount?.value) { - return ( - <> - - - - - ); - } - if (isGroupSelected) { return ( <> @@ -87,21 +74,18 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { passed={totalPassedFindings} failed={totalFailedFindings} /> - {isGroupLoading ? ( - defaultLoadingRenderer() - ) : ( - - )} +
); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index b83485f1cb469..be5253cc710b7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -218,6 +218,9 @@ export const useLatestFindingsGrouping = ({ }); }; + const isEmptyResults = + !isFetching && isFindingsRootGroupingAggregation(groupData) && !groupData.unitsCount?.value; + return { groupData, grouping, @@ -236,5 +239,6 @@ export const useLatestFindingsGrouping = ({ onDistributionBarClick, totalPassedFindings, totalFailedFindings, + isEmptyResults, }; };