mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
20ed95d082
commit
5d833360f6
3 changed files with 157 additions and 56 deletions
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue