From 917d2cf09f73f5e45b75f45355c3485cae0726dd Mon Sep 17 00:00:00 2001 From: Paulo Silva Date: Fri, 20 Jun 2025 16:04:42 -0700 Subject: [PATCH] [Asset Inventory] Handling Asset Criticality soft delete mechanism (#224630) ## Summary This PR updates Asset Inventory Grouping and Datagrid functionalities to accommodate the `asset.criticality` soft delete mechanisms, which sets the value of `asset.criticality` to "delete" once an entity is manually "Unassigned" after having a value before: - Added condition on grouping to treat both "deleted" and "missing value" as Unassigned. - Added rendering condition on the datatable to display the Unassigned badge when `asset.criticality` value is either missing or "deleted". ### Recording https://github.com/user-attachments/assets/37d7e44f-cb57-4c29-b49d-cda9b341497d --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/asset_inventory_data_table.tsx | 12 ++- .../asset_inventory_table_section.tsx | 84 +++++++++++++---- .../grouping/use_asset_inventory_grouping.ts | 91 +++++++++++++++---- .../utils/asset_inventory_group_renderer.tsx | 15 +-- .../asset_inventory_data_client.ts | 20 ++-- 5 files changed, 171 insertions(+), 51 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_data_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_data_table.tsx index 4029338a0988..59a4decdd52d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_data_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_data_table.tsx @@ -97,10 +97,14 @@ const customCellRenderer = (rows: DataTableRecord[]): CustomCellRenderer => ({ return ; }, [ASSET_FIELDS.ASSET_CRITICALITY]: ({ rowIndex }: EuiDataGridCellValueElementProps) => { - const criticality = rows[rowIndex].flattened[ - ASSET_FIELDS.ASSET_CRITICALITY - ] as CriticalityLevelWithUnassigned; - return ; + const criticality = rows[rowIndex].flattened[ASSET_FIELDS.ASSET_CRITICALITY] as + | CriticalityLevelWithUnassigned + | 'deleted'; + return ( + + ); }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_table_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_table_section.tsx index 92e94082c456..1b9bf66a4042 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_table_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/asset_inventory_table_section.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect } from 'react'; import type { Filter } from '@kbn/es-query'; import type { AssetInventoryURLStateResult } from '../hooks/use_asset_inventory_url_state/use_asset_inventory_url_state'; -import { DEFAULT_TABLE_SECTION_HEIGHT } from '../constants'; +import { ASSET_FIELDS, DEFAULT_TABLE_SECTION_HEIGHT } from '../constants'; import { GroupWrapper } from './grouping/asset_inventory_grouping'; import { useAssetInventoryGrouping } from './grouping/use_asset_inventory_grouping'; import { AssetInventoryDataTable } from './asset_inventory_data_table'; @@ -82,9 +82,9 @@ const GroupWithURLPagination = ({ ( + renderChildComponent={(currentGroupFilters) => ( { + const query = filter?.query; + if (query?.bool?.should?.[0]?.bool?.must_not?.exists?.field === ASSET_FIELDS.ASSET_CRITICALITY) { + return { + meta: filter?.meta ?? { alias: null, disabled: false, negate: false }, + query: { + bool: { + filter: { + bool: { + should: [ + { term: { [ASSET_FIELDS.ASSET_CRITICALITY]: 'deleted' } }, + { bool: { must_not: { exists: { field: ASSET_FIELDS.ASSET_CRITICALITY } } } }, + ], + minimum_should_match: 1, + }, + }, + }, + }, + }; + } + return query?.match_phrase || query?.bool?.should || query?.bool?.filter ? filter : null; +}; + +const filterTypeGuard = (filter: Filter | null): filter is Filter => filter !== null; + +const mergeCurrentAndParentFilters = ( + currentGroupFilters: Filter[], + parentGroupFilters: string | undefined +) => { + return [...currentGroupFilters, ...(parentGroupFilters ? JSON.parse(parentGroupFilters) : [])]; +}; + const GroupContent = ({ currentGroupFilters, state, @@ -122,16 +160,21 @@ const GroupContent = ({ }: GroupContentProps) => { if (groupingLevel < selectedGroupOptions.length) { const nextGroupingLevel = groupingLevel + 1; + + const newParentGroupFilters = mergeCurrentAndParentFilters( + currentGroupFilters, + parentGroupFilters + ) + .map(groupFilterMap) + .filter(filterTypeGuard); + return ( ); @@ -162,10 +205,12 @@ const GroupWithLocalPagination = ({ const [subgroupPageIndex, setSubgroupPageIndex] = useState(0); const [subgroupPageSize, setSubgroupPageSize] = useState(10); + const groupFilters = parentGroupFilters ? JSON.parse(parentGroupFilters) : []; + const { groupData, grouping, isFetching } = useAssetInventoryGrouping({ state: { ...state, pageIndex: subgroupPageIndex, pageSize: subgroupPageSize }, selectedGroup, - groupFilters: parentGroupFilters ? JSON.parse(parentGroupFilters) : [], + groupFilters, }); /** @@ -180,13 +225,14 @@ const GroupWithLocalPagination = ({ ( + renderChildComponent={(currentGroupFilters) => ( )} activePageIndex={subgroupPageIndex} @@ -207,6 +253,13 @@ interface DataTableWithLocalPagination { parentGroupFilters?: string; } +const getDataGridFilter = (filter: Filter | null) => { + if (!filter) return null; + return { + ...(filter?.query ?? {}), + }; +}; + const DataTableWithLocalPagination = ({ state, currentGroupFilters, @@ -215,12 +268,11 @@ const DataTableWithLocalPagination = ({ const [tablePageIndex, setTablePageIndex] = useState(0); const [tablePageSize, setTablePageSize] = useState(10); - const combinedFilters = [ - ...currentGroupFilters, - ...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []), - ] - .map(({ query }) => (query?.match_phrase || query?.bool?.should ? query : null)) - .filter(Boolean); + const combinedFilters = mergeCurrentAndParentFilters(currentGroupFilters, parentGroupFilters) + .map(groupFilterMap) + .filter(filterTypeGuard) + .map(getDataGridFilter) + .filter((filter): filter is NonNullable => Boolean(filter)); const newState: AssetInventoryURLStateResult = { ...state, diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/use_asset_inventory_grouping.ts b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/use_asset_inventory_grouping.ts index 1cdecad2c7c4..67692131c22b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/use_asset_inventory_grouping.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/use_asset_inventory_grouping.ts @@ -22,6 +22,8 @@ import { } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { METRIC_TYPE } from '@kbn/analytics'; +import dedent from 'dedent'; +import type { MappingRuntimeFieldType } from '@elastic/elasticsearch/lib/api/types'; import { useDataViewContext } from '../../hooks/data_view_context'; import type { AssetInventoryURLStateResult, @@ -159,24 +161,81 @@ export const useAssetInventoryGrouping = ({ // This is recommended by the grouping component to cover an edge case where `selectedGroup` has multiple values const uniqueValue = useMemo(() => `${selectedGroup}-${uuid.v4()}`, [selectedGroup]); - const groupingQuery = getGroupingQuery({ - additionalFilters: query ? [query, additionalFilters] : [additionalFilters], - groupByField: currentSelectedGroup, - uniqueValue, - pageNumber: pageIndex * pageSize, - size: pageSize, - sort: [{ groupByField: { order: 'desc' } }], - statsAggregations: getAggregationsByGroupField(currentSelectedGroup), - rootAggregations: [ - { - ...(!isNoneGroup([currentSelectedGroup]) && { - nullGroupItems: { - missing: { field: currentSelectedGroup }, + const groupingQuery = useMemo( + () => ({ + ...getGroupingQuery({ + additionalFilters: query ? [query, additionalFilters] : [additionalFilters], + groupByField: currentSelectedGroup, + uniqueValue, + pageNumber: pageIndex * pageSize, + size: pageSize, + sort: [{ groupByField: { order: 'desc' } }], + statsAggregations: getAggregationsByGroupField(currentSelectedGroup), + rootAggregations: [ + { + ...(!isNoneGroup([currentSelectedGroup]) && { + nullGroupItems: + currentSelectedGroup === ASSET_FIELDS.ASSET_CRITICALITY + ? { + filter: { + bool: { + should: [ + { term: { [ASSET_FIELDS.ASSET_CRITICALITY]: 'deleted' } }, + { + bool: { + must_not: { exists: { field: ASSET_FIELDS.ASSET_CRITICALITY } }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + } + : { missing: { field: currentSelectedGroup } }, + }), }, - }), + ], + }), + runtime_mappings: { + groupByField: { + type: 'keyword' as MappingRuntimeFieldType, + script: { + source: dedent(` + def groupValues = []; + if (doc.containsKey(params['selectedGroup']) && !doc[params['selectedGroup']].empty) { + groupValues = doc[params['selectedGroup']]; + } + // If selectedGroup is 'asset.criticality', treat 'deleted' as undefined group + boolean treatAsUndefined = false; + int count = groupValues.size(); + if (params['selectedGroup'] == 'asset.criticality') { + boolean isDeleted = false; + for (def v : groupValues) { + if (v == 'deleted') { + isDeleted = true; + break; + } + } + treatAsUndefined = (count == 0 || count > 100 || isDeleted); + } else { + treatAsUndefined = (count == 0 || count > 100); + } + if (treatAsUndefined) { + emit(params['uniqueValue']); + } else { + emit(groupValues.join(params['uniqueValue'])); + } + `), + params: { + selectedGroup: currentSelectedGroup, + uniqueValue, + }, + }, + }, }, - ], - }); + }), + [currentSelectedGroup, uniqueValue, additionalFilters, query, pageIndex, pageSize] + ); const { data, isFetching } = useFetchGroupedData({ query: groupingQuery, diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/utils/asset_inventory_group_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/utils/asset_inventory_group_renderer.tsx index 07b19d4e73b6..4bbd3144a0ed 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/utils/asset_inventory_group_renderer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/grouping/utils/asset_inventory_group_renderer.tsx @@ -69,6 +69,13 @@ export const groupPanelRenderer: GroupPanelRenderer = switch (selectedGroup) { case ASSET_GROUPING_OPTIONS.ASSET_CRITICALITY: + const rawCriticalityLevel = firstNonNullValue(bucket.assetCriticality?.buckets?.[0]?.key) as + | CriticalityLevelWithUnassigned + | 'deleted'; + + const criticalityLevel = + rawCriticalityLevel === 'deleted' ? 'unassigned' : rawCriticalityLevel; + return nullGroupMessage ? ( @@ -84,13 +91,7 @@ export const groupPanelRenderer: GroupPanelRenderer = - + diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/asset_inventory/asset_inventory_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/asset_inventory/asset_inventory_data_client.ts index ebb46ec784f1..aed5e6aebc19 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/asset_inventory/asset_inventory_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/asset_inventory/asset_inventory_data_client.ts @@ -165,14 +165,18 @@ export class AssetInventoryDataClient { } } - await installDataView( - secSolutionContext.getSpaceId(), - secSolutionContext.getDataViewsService(), - ASSET_INVENTORY_DATA_VIEW_NAME, - ASSET_INVENTORY_INDEX_PATTERN, - ASSET_INVENTORY_DATA_VIEW_ID_PREFIX, - logger - ); + try { + await installDataView( + secSolutionContext.getSpaceId(), + secSolutionContext.getDataViewsService(), + ASSET_INVENTORY_DATA_VIEW_NAME, + ASSET_INVENTORY_INDEX_PATTERN, + ASSET_INVENTORY_DATA_VIEW_ID_PREFIX, + logger + ); + } catch (error) { + logger.error(`Error installing asset inventory data view: ${error.message}`); + } logger.debug(`Enabled asset inventory`);