[Cloud Security] add vulnerability subgrouping (#176351)

## Summary

This feature adds subgrouping to the Vulnerabilities tab.
 * Enable subgrouping for three levels
 * Pagination between grouping levels
 * Group Selector can show up three data fields
 

73f69768-27cf-408e-bbb3-1a1d17a43838
This commit is contained in:
Lola 2024-02-09 10:58:28 -05:00 committed by GitHub
parent 20ed95d082
commit 5d833360f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 157 additions and 56 deletions

View file

@ -14,6 +14,7 @@ import {
parseGroupingQuery,
} from '@kbn/securitysolution-grouping/src';
import { useMemo } from 'react';
import { buildEsQuery, Filter } from '@kbn/es-query';
import { LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY } from '../../../common/constants';
import { useDataViewContext } from '../../../common/contexts/data_view_context';
import {
@ -110,9 +111,15 @@ export const isVulnerabilitiesRootGroupingAggregation = (
export const useLatestVulnerabilitiesGrouping = ({
groupPanelRenderer,
groupStatsRenderer,
groupingLevel = 0,
groupFilters = [],
selectedGroup,
}: {
groupPanelRenderer?: GroupPanelRenderer<VulnerabilitiesGroupingAggregation>;
groupStatsRenderer?: GroupStatsRenderer<VulnerabilitiesGroupingAggregation>;
groupingLevel?: number;
groupFilters?: Filter[];
selectedGroup?: string;
}) => {
const { dataView } = useDataViewContext();
@ -121,7 +128,6 @@ export const useLatestVulnerabilitiesGrouping = ({
grouping,
pageSize,
query,
selectedGroup,
onChangeGroupsItemsPerPage,
onChangeGroupsPage,
setUrlQuery,
@ -130,6 +136,7 @@ export const useLatestVulnerabilitiesGrouping = ({
onResetFilters,
error,
filters,
setActivePageIndex,
} = useCloudSecurityGrouping({
dataView,
groupingTitle,
@ -139,19 +146,22 @@ export const useLatestVulnerabilitiesGrouping = ({
groupPanelRenderer,
groupStatsRenderer,
groupingLocalStorageKey: LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY,
maxGroupingLevels: 1,
groupingLevel,
});
const additionalFilters = buildEsQuery(dataView, [], groupFilters);
const currentSelectedGroup = selectedGroup || grouping.selectedGroups[0];
const groupingQuery = getGroupingQuery({
additionalFilters: query ? [query] : [],
groupByField: selectedGroup,
additionalFilters: query ? [query, additionalFilters] : [additionalFilters],
groupByField: currentSelectedGroup,
uniqueValue,
from: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`,
to: 'now',
pageNumber: activePageIndex * pageSize,
size: pageSize,
sort: [{ groupByField: { order: 'desc' } }],
statsAggregations: getAggregationsByGroupField(selectedGroup),
statsAggregations: getAggregationsByGroupField(currentSelectedGroup),
});
const { data, isFetching } = useGroupedVulnerabilities({
@ -162,11 +172,11 @@ export const useLatestVulnerabilitiesGrouping = ({
const groupData = useMemo(
() =>
parseGroupingQuery(
selectedGroup,
currentSelectedGroup,
uniqueValue,
data as GroupingAggregation<VulnerabilitiesGroupingAggregation>
),
[data, selectedGroup, uniqueValue]
[data, currentSelectedGroup, uniqueValue]
);
const isEmptyResults =
@ -179,6 +189,7 @@ export const useLatestVulnerabilitiesGrouping = ({
grouping,
isFetching,
activePageIndex,
setActivePageIndex,
pageSize,
selectedGroup,
onChangeGroupsItemsPerPage,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Filter } from '@kbn/es-query';
import React from 'react';
import React, { useEffect } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { useLatestVulnerabilitiesGrouping } from './hooks/use_latest_vulnerabilities_grouping';
import { LatestVulnerabilitiesTable } from './latest_vulnerabilities_table';
@ -17,31 +17,129 @@ import { CloudSecurityGrouping } from '../../components/cloud_security_grouping'
import { DEFAULT_GROUPING_TABLE_HEIGHT } from '../../common/constants';
export const LatestVulnerabilitiesContainer = () => {
const renderChildComponent = (groupFilters: Filter[]) => {
const SubGrouping = ({
renderChildComponent,
groupingLevel,
parentGroupFilters,
selectedGroup,
groupSelectorComponent,
}: {
renderChildComponent: (groupFilters: Filter[]) => JSX.Element;
groupingLevel: number;
parentGroupFilters?: string;
selectedGroup: string;
groupSelectorComponent?: JSX.Element;
}) => {
const {
groupData,
grouping,
isFetching,
activePageIndex,
pageSize,
onChangeGroupsItemsPerPage,
onChangeGroupsPage,
isGroupLoading,
setActivePageIndex,
} = useLatestVulnerabilitiesGrouping({
groupPanelRenderer,
groupStatsRenderer,
groupingLevel,
selectedGroup,
groupFilters: parentGroupFilters ? JSON.parse(parentGroupFilters) : [],
});
/**
* This is used to reset the active page index when the selected group changes
* It is needed because the grouping number of pages can change according to the selected group
*/
useEffect(() => {
setActivePageIndex(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedGroup]);
return (
<LatestVulnerabilitiesTable
nonPersistedFilters={groupFilters}
height={DEFAULT_GROUPING_TABLE_HEIGHT}
<CloudSecurityGrouping
data={groupData}
grouping={grouping}
renderChildComponent={renderChildComponent}
onChangeGroupsItemsPerPage={onChangeGroupsItemsPerPage}
onChangeGroupsPage={onChangeGroupsPage}
activePageIndex={activePageIndex}
isFetching={isFetching}
pageSize={pageSize}
selectedGroup={selectedGroup}
isGroupLoading={isGroupLoading}
groupingLevel={groupingLevel}
groupSelectorComponent={groupSelectorComponent}
/>
);
};
const {
isGroupSelected,
groupData,
grouping,
isFetching,
activePageIndex,
pageSize,
selectedGroup,
onChangeGroupsItemsPerPage,
onChangeGroupsPage,
setUrlQuery,
isGroupLoading,
onResetFilters,
error,
isEmptyResults,
} = useLatestVulnerabilitiesGrouping({ groupPanelRenderer, groupStatsRenderer });
const renderChildComponent = ({
level,
currentSelectedGroup,
selectedGroupOptions,
parentGroupFilters,
groupSelectorComponent,
}: {
level: number;
currentSelectedGroup: string;
selectedGroupOptions: string[];
parentGroupFilters?: string;
groupSelectorComponent?: JSX.Element;
}) => {
let getChildComponent;
if (currentSelectedGroup === 'none') {
return (
<LatestVulnerabilitiesTable
groupSelectorComponent={groupSelectorComponent}
nonPersistedFilters={[...(parentGroupFilters ? JSON.parse(parentGroupFilters) : [])]}
height={DEFAULT_GROUPING_TABLE_HEIGHT}
/>
);
}
if (level < selectedGroupOptions.length - 1 && !selectedGroupOptions.includes('none')) {
getChildComponent = (currentGroupFilters: Filter[]) => {
const nextGroupingLevel = level + 1;
return renderChildComponent({
level: nextGroupingLevel,
currentSelectedGroup: selectedGroupOptions[nextGroupingLevel],
selectedGroupOptions,
parentGroupFilters: JSON.stringify([
...currentGroupFilters,
...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []),
]),
groupSelectorComponent,
});
};
} else {
getChildComponent = (currentGroupFilters: Filter[]) => {
return (
<LatestVulnerabilitiesTable
nonPersistedFilters={[
...currentGroupFilters,
...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []),
]}
height={DEFAULT_GROUPING_TABLE_HEIGHT}
/>
);
};
}
return (
<SubGrouping
renderChildComponent={getChildComponent}
selectedGroup={selectedGroupOptions[level]}
groupingLevel={level}
parentGroupFilters={parentGroupFilters}
groupSelectorComponent={groupSelectorComponent}
/>
);
};
const { grouping, isFetching, setUrlQuery, onResetFilters, error, isEmptyResults } =
useLatestVulnerabilitiesGrouping({ groupPanelRenderer, groupStatsRenderer });
if (error || isEmptyResults) {
return (
@ -53,34 +151,18 @@ export const LatestVulnerabilitiesContainer = () => {
</>
);
}
if (isGroupSelected) {
return (
<>
<FindingsSearchBar setQuery={setUrlQuery} loading={isFetching} />
<div>
<EuiSpacer size="m" />
<CloudSecurityGrouping
data={groupData}
grouping={grouping}
renderChildComponent={renderChildComponent}
onChangeGroupsItemsPerPage={onChangeGroupsItemsPerPage}
onChangeGroupsPage={onChangeGroupsPage}
activePageIndex={activePageIndex}
isFetching={isFetching}
pageSize={pageSize}
selectedGroup={selectedGroup}
isGroupLoading={isGroupLoading}
/>
</div>
</>
);
}
return (
<>
<FindingsSearchBar setQuery={setUrlQuery} loading={isFetching} />
<EuiSpacer size="m" />
<LatestVulnerabilitiesTable groupSelectorComponent={grouping.groupSelector} />
<div>
{renderChildComponent({
level: 0,
currentSelectedGroup: grouping.selectedGroups[0],
selectedGroupOptions: grouping.selectedGroups,
groupSelectorComponent: grouping.groupSelector,
})}
</div>
</>
);
};

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import expect from '@kbn/expect';
import type { FtrProviderContext } from '../ftr_provider_context';
import { vulnerabilitiesLatestMock } from '../mocks/vulnerabilities_latest_mock';
@ -44,9 +44,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
after(async () => {
const groupSelector = await findings.groupSelector();
await groupSelector.openDropDown();
await groupSelector.setValue('None');
await findings.vulnerabilitiesIndex.remove();
});
@ -95,6 +92,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('groups vulnerabilities by CVE and sort by number of vulnerabilities desc', async () => {
const groupSelector = findings.groupSelector();
await groupSelector.openDropDown();
await groupSelector.setValue('None');
await groupSelector.openDropDown();
await groupSelector.setValue('CVE');
const grouping = await findings.findingsGrouping();
@ -133,6 +132,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('groups vulnerabilities by resource and sort by number of vulnerabilities desc', async () => {
const groupSelector = findings.groupSelector();
await groupSelector.openDropDown();
await groupSelector.setValue('None');
await groupSelector.openDropDown();
await groupSelector.setValue('Resource');
const grouping = await findings.findingsGrouping();
@ -172,7 +173,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('SearchBar', () => {
it('add filter', async () => {
const groupSelector = await findings.groupSelector();
await groupSelector.openDropDown();
await groupSelector.setValue('None');
await groupSelector.openDropDown();
await groupSelector.setValue('Resource');
// Filter bar uses the field's customLabel in the DataView
await filterBar.addFilter({
field: 'Resource Name',
operation: 'is',