mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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} />;
|
return <RiskBadge risk={risk} />;
|
||||||
},
|
},
|
||||||
[ASSET_FIELDS.ASSET_CRITICALITY]: ({ rowIndex }: EuiDataGridCellValueElementProps) => {
|
[ASSET_FIELDS.ASSET_CRITICALITY]: ({ rowIndex }: EuiDataGridCellValueElementProps) => {
|
||||||
const criticality = rows[rowIndex].flattened[
|
const criticality = rows[rowIndex].flattened[ASSET_FIELDS.ASSET_CRITICALITY] as
|
||||||
ASSET_FIELDS.ASSET_CRITICALITY
|
| CriticalityLevelWithUnassigned
|
||||||
] as CriticalityLevelWithUnassigned;
|
| 'deleted';
|
||||||
return <AssetCriticalityBadge criticalityLevel={criticality} />;
|
return (
|
||||||
|
<AssetCriticalityBadge
|
||||||
|
criticalityLevel={criticality === 'deleted' ? 'unassigned' : criticality}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import type { Filter } from '@kbn/es-query';
|
import type { Filter } from '@kbn/es-query';
|
||||||
import type { AssetInventoryURLStateResult } from '../hooks/use_asset_inventory_url_state/use_asset_inventory_url_state';
|
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 { GroupWrapper } from './grouping/asset_inventory_grouping';
|
||||||
import { useAssetInventoryGrouping } from './grouping/use_asset_inventory_grouping';
|
import { useAssetInventoryGrouping } from './grouping/use_asset_inventory_grouping';
|
||||||
import { AssetInventoryDataTable } from './asset_inventory_data_table';
|
import { AssetInventoryDataTable } from './asset_inventory_data_table';
|
||||||
|
@ -82,9 +82,9 @@ const GroupWithURLPagination = ({
|
||||||
<GroupWrapper
|
<GroupWrapper
|
||||||
data={groupData}
|
data={groupData}
|
||||||
grouping={grouping}
|
grouping={grouping}
|
||||||
renderChildComponent={(childFilters) => (
|
renderChildComponent={(currentGroupFilters) => (
|
||||||
<GroupContent
|
<GroupContent
|
||||||
currentGroupFilters={childFilters}
|
currentGroupFilters={currentGroupFilters}
|
||||||
state={state}
|
state={state}
|
||||||
groupingLevel={1}
|
groupingLevel={1}
|
||||||
selectedGroupOptions={selectedGroupOptions}
|
selectedGroupOptions={selectedGroupOptions}
|
||||||
|
@ -112,6 +112,44 @@ interface GroupContentProps {
|
||||||
groupSelectorComponent?: JSX.Element;
|
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 = ({
|
const GroupContent = ({
|
||||||
currentGroupFilters,
|
currentGroupFilters,
|
||||||
state,
|
state,
|
||||||
|
@ -122,16 +160,21 @@ const GroupContent = ({
|
||||||
}: GroupContentProps) => {
|
}: GroupContentProps) => {
|
||||||
if (groupingLevel < selectedGroupOptions.length) {
|
if (groupingLevel < selectedGroupOptions.length) {
|
||||||
const nextGroupingLevel = groupingLevel + 1;
|
const nextGroupingLevel = groupingLevel + 1;
|
||||||
|
|
||||||
|
const newParentGroupFilters = mergeCurrentAndParentFilters(
|
||||||
|
currentGroupFilters,
|
||||||
|
parentGroupFilters
|
||||||
|
)
|
||||||
|
.map(groupFilterMap)
|
||||||
|
.filter(filterTypeGuard);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupWithLocalPagination
|
<GroupWithLocalPagination
|
||||||
state={state}
|
state={state}
|
||||||
groupingLevel={nextGroupingLevel}
|
groupingLevel={nextGroupingLevel}
|
||||||
selectedGroup={selectedGroupOptions[groupingLevel]}
|
selectedGroup={selectedGroupOptions[groupingLevel]}
|
||||||
selectedGroupOptions={selectedGroupOptions}
|
selectedGroupOptions={selectedGroupOptions}
|
||||||
parentGroupFilters={JSON.stringify([
|
parentGroupFilters={JSON.stringify(newParentGroupFilters)}
|
||||||
...currentGroupFilters,
|
|
||||||
...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []),
|
|
||||||
])}
|
|
||||||
groupSelectorComponent={groupSelectorComponent}
|
groupSelectorComponent={groupSelectorComponent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -162,10 +205,12 @@ const GroupWithLocalPagination = ({
|
||||||
const [subgroupPageIndex, setSubgroupPageIndex] = useState(0);
|
const [subgroupPageIndex, setSubgroupPageIndex] = useState(0);
|
||||||
const [subgroupPageSize, setSubgroupPageSize] = useState(10);
|
const [subgroupPageSize, setSubgroupPageSize] = useState(10);
|
||||||
|
|
||||||
|
const groupFilters = parentGroupFilters ? JSON.parse(parentGroupFilters) : [];
|
||||||
|
|
||||||
const { groupData, grouping, isFetching } = useAssetInventoryGrouping({
|
const { groupData, grouping, isFetching } = useAssetInventoryGrouping({
|
||||||
state: { ...state, pageIndex: subgroupPageIndex, pageSize: subgroupPageSize },
|
state: { ...state, pageIndex: subgroupPageIndex, pageSize: subgroupPageSize },
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
groupFilters: parentGroupFilters ? JSON.parse(parentGroupFilters) : [],
|
groupFilters,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -180,13 +225,14 @@ const GroupWithLocalPagination = ({
|
||||||
<GroupWrapper
|
<GroupWrapper
|
||||||
data={groupData}
|
data={groupData}
|
||||||
grouping={grouping}
|
grouping={grouping}
|
||||||
renderChildComponent={(childFilters) => (
|
renderChildComponent={(currentGroupFilters) => (
|
||||||
<GroupContent
|
<GroupContent
|
||||||
currentGroupFilters={childFilters}
|
currentGroupFilters={currentGroupFilters.map(groupFilterMap).filter(filterTypeGuard)}
|
||||||
state={state}
|
state={state}
|
||||||
groupingLevel={groupingLevel}
|
groupingLevel={groupingLevel}
|
||||||
selectedGroupOptions={selectedGroupOptions}
|
selectedGroupOptions={selectedGroupOptions}
|
||||||
groupSelectorComponent={groupSelectorComponent}
|
groupSelectorComponent={groupSelectorComponent}
|
||||||
|
parentGroupFilters={JSON.stringify(groupFilters)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
activePageIndex={subgroupPageIndex}
|
activePageIndex={subgroupPageIndex}
|
||||||
|
@ -207,6 +253,13 @@ interface DataTableWithLocalPagination {
|
||||||
parentGroupFilters?: string;
|
parentGroupFilters?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDataGridFilter = (filter: Filter | null) => {
|
||||||
|
if (!filter) return null;
|
||||||
|
return {
|
||||||
|
...(filter?.query ?? {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const DataTableWithLocalPagination = ({
|
const DataTableWithLocalPagination = ({
|
||||||
state,
|
state,
|
||||||
currentGroupFilters,
|
currentGroupFilters,
|
||||||
|
@ -215,12 +268,11 @@ const DataTableWithLocalPagination = ({
|
||||||
const [tablePageIndex, setTablePageIndex] = useState(0);
|
const [tablePageIndex, setTablePageIndex] = useState(0);
|
||||||
const [tablePageSize, setTablePageSize] = useState(10);
|
const [tablePageSize, setTablePageSize] = useState(10);
|
||||||
|
|
||||||
const combinedFilters = [
|
const combinedFilters = mergeCurrentAndParentFilters(currentGroupFilters, parentGroupFilters)
|
||||||
...currentGroupFilters,
|
.map(groupFilterMap)
|
||||||
...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []),
|
.filter(filterTypeGuard)
|
||||||
]
|
.map(getDataGridFilter)
|
||||||
.map(({ query }) => (query?.match_phrase || query?.bool?.should ? query : null))
|
.filter((filter): filter is NonNullable<Filter['query']> => Boolean(filter));
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const newState: AssetInventoryURLStateResult = {
|
const newState: AssetInventoryURLStateResult = {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -22,6 +22,8 @@ import {
|
||||||
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
|
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
|
||||||
import { METRIC_TYPE } from '@kbn/analytics';
|
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 { useDataViewContext } from '../../hooks/data_view_context';
|
||||||
import type {
|
import type {
|
||||||
AssetInventoryURLStateResult,
|
AssetInventoryURLStateResult,
|
||||||
|
@ -159,7 +161,9 @@ export const useAssetInventoryGrouping = ({
|
||||||
// This is recommended by the grouping component to cover an edge case where `selectedGroup` has multiple values
|
// 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 uniqueValue = useMemo(() => `${selectedGroup}-${uuid.v4()}`, [selectedGroup]);
|
||||||
|
|
||||||
const groupingQuery = getGroupingQuery({
|
const groupingQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...getGroupingQuery({
|
||||||
additionalFilters: query ? [query, additionalFilters] : [additionalFilters],
|
additionalFilters: query ? [query, additionalFilters] : [additionalFilters],
|
||||||
groupByField: currentSelectedGroup,
|
groupByField: currentSelectedGroup,
|
||||||
uniqueValue,
|
uniqueValue,
|
||||||
|
@ -170,13 +174,68 @@ export const useAssetInventoryGrouping = ({
|
||||||
rootAggregations: [
|
rootAggregations: [
|
||||||
{
|
{
|
||||||
...(!isNoneGroup([currentSelectedGroup]) && {
|
...(!isNoneGroup([currentSelectedGroup]) && {
|
||||||
nullGroupItems: {
|
nullGroupItems:
|
||||||
missing: { field: currentSelectedGroup },
|
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({
|
const { data, isFetching } = useFetchGroupedData({
|
||||||
query: groupingQuery,
|
query: groupingQuery,
|
||||||
|
|
|
@ -69,6 +69,13 @@ export const groupPanelRenderer: GroupPanelRenderer<AssetsGroupingAggregation> =
|
||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case ASSET_GROUPING_OPTIONS.ASSET_CRITICALITY:
|
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 ? (
|
return nullGroupMessage ? (
|
||||||
<EuiFlexGroup alignItems="center">
|
<EuiFlexGroup alignItems="center">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
|
@ -84,13 +91,7 @@ export const groupPanelRenderer: GroupPanelRenderer<AssetsGroupingAggregation> =
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiFlexGroup direction="column" gutterSize="none">
|
<EuiFlexGroup direction="column" gutterSize="none">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<AssetCriticalityBadge
|
<AssetCriticalityBadge criticalityLevel={criticalityLevel} />
|
||||||
criticalityLevel={
|
|
||||||
firstNonNullValue(
|
|
||||||
bucket.assetCriticality?.buckets?.[0]?.key
|
|
||||||
) as CriticalityLevelWithUnassigned
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
|
@ -165,6 +165,7 @@ export class AssetInventoryDataClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await installDataView(
|
await installDataView(
|
||||||
secSolutionContext.getSpaceId(),
|
secSolutionContext.getSpaceId(),
|
||||||
secSolutionContext.getDataViewsService(),
|
secSolutionContext.getDataViewsService(),
|
||||||
|
@ -173,6 +174,9 @@ export class AssetInventoryDataClient {
|
||||||
ASSET_INVENTORY_DATA_VIEW_ID_PREFIX,
|
ASSET_INVENTORY_DATA_VIEW_ID_PREFIX,
|
||||||
logger
|
logger
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error installing asset inventory data view: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(`Enabled asset inventory`);
|
logger.debug(`Enabled asset inventory`);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue