mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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>
This commit is contained in:
parent
22f115e476
commit
917d2cf09f
5 changed files with 171 additions and 51 deletions
|
@ -97,10 +97,14 @@ const customCellRenderer = (rows: DataTableRecord[]): CustomCellRenderer => ({
|
|||
return <RiskBadge risk={risk} />;
|
||||
},
|
||||
[ASSET_FIELDS.ASSET_CRITICALITY]: ({ rowIndex }: EuiDataGridCellValueElementProps) => {
|
||||
const criticality = rows[rowIndex].flattened[
|
||||
ASSET_FIELDS.ASSET_CRITICALITY
|
||||
] as CriticalityLevelWithUnassigned;
|
||||
return <AssetCriticalityBadge criticalityLevel={criticality} />;
|
||||
const criticality = rows[rowIndex].flattened[ASSET_FIELDS.ASSET_CRITICALITY] as
|
||||
| CriticalityLevelWithUnassigned
|
||||
| 'deleted';
|
||||
return (
|
||||
<AssetCriticalityBadge
|
||||
criticalityLevel={criticality === 'deleted' ? 'unassigned' : criticality}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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 = ({
|
|||
<GroupWrapper
|
||||
data={groupData}
|
||||
grouping={grouping}
|
||||
renderChildComponent={(childFilters) => (
|
||||
renderChildComponent={(currentGroupFilters) => (
|
||||
<GroupContent
|
||||
currentGroupFilters={childFilters}
|
||||
currentGroupFilters={currentGroupFilters}
|
||||
state={state}
|
||||
groupingLevel={1}
|
||||
selectedGroupOptions={selectedGroupOptions}
|
||||
|
@ -112,6 +112,44 @@ interface GroupContentProps {
|
|||
groupSelectorComponent?: JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to modify the filters for the asset inventory grouping
|
||||
* It is needed because the asset.criticality field has a soft delete mechanism
|
||||
* So asset.criticality = "deleted" should be excluded from the grouping
|
||||
* (treated as a missing value)
|
||||
*/
|
||||
const groupFilterMap = (filter: Filter | null): Filter | null => {
|
||||
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 (
|
||||
<GroupWithLocalPagination
|
||||
state={state}
|
||||
groupingLevel={nextGroupingLevel}
|
||||
selectedGroup={selectedGroupOptions[groupingLevel]}
|
||||
selectedGroupOptions={selectedGroupOptions}
|
||||
parentGroupFilters={JSON.stringify([
|
||||
...currentGroupFilters,
|
||||
...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []),
|
||||
])}
|
||||
parentGroupFilters={JSON.stringify(newParentGroupFilters)}
|
||||
groupSelectorComponent={groupSelectorComponent}
|
||||
/>
|
||||
);
|
||||
|
@ -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 = ({
|
|||
<GroupWrapper
|
||||
data={groupData}
|
||||
grouping={grouping}
|
||||
renderChildComponent={(childFilters) => (
|
||||
renderChildComponent={(currentGroupFilters) => (
|
||||
<GroupContent
|
||||
currentGroupFilters={childFilters}
|
||||
currentGroupFilters={currentGroupFilters.map(groupFilterMap).filter(filterTypeGuard)}
|
||||
state={state}
|
||||
groupingLevel={groupingLevel}
|
||||
selectedGroupOptions={selectedGroupOptions}
|
||||
groupSelectorComponent={groupSelectorComponent}
|
||||
parentGroupFilters={JSON.stringify(groupFilters)}
|
||||
/>
|
||||
)}
|
||||
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<Filter['query']> => Boolean(filter));
|
||||
|
||||
const newState: AssetInventoryURLStateResult = {
|
||||
...state,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -69,6 +69,13 @@ export const groupPanelRenderer: GroupPanelRenderer<AssetsGroupingAggregation> =
|
|||
|
||||
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 ? (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
|
@ -84,13 +91,7 @@ export const groupPanelRenderer: GroupPanelRenderer<AssetsGroupingAggregation> =
|
|||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<AssetCriticalityBadge
|
||||
criticalityLevel={
|
||||
firstNonNullValue(
|
||||
bucket.assetCriticality?.buckets?.[0]?.key
|
||||
) as CriticalityLevelWithUnassigned
|
||||
}
|
||||
/>
|
||||
<AssetCriticalityBadge criticalityLevel={criticalityLevel} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -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`);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue