Skip to content

Commit

Permalink
[Cloud Security] add vulnerability subgrouping (elastic#176351)
Browse files Browse the repository at this point in the history
## 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
 

https://github.com/elastic/kibana/assets/17135495/73f69768-27cf-408e-bbb3-1a1d17a43838
  • Loading branch information
Omolola-Akinleye authored Feb 9, 2024
1 parent 20ed95d commit 5d83336
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();

Expand All @@ -121,7 +128,6 @@ export const useLatestVulnerabilitiesGrouping = ({
grouping,
pageSize,
query,
selectedGroup,
onChangeGroupsItemsPerPage,
onChangeGroupsPage,
setUrlQuery,
Expand All @@ -130,6 +136,7 @@ export const useLatestVulnerabilitiesGrouping = ({
onResetFilters,
error,
filters,
setActivePageIndex,
} = useCloudSecurityGrouping({
dataView,
groupingTitle,
Expand All @@ -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({
Expand All @@ -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 =
Expand All @@ -179,6 +189,7 @@ export const useLatestVulnerabilitiesGrouping = ({
grouping,
isFetching,
activePageIndex,
setActivePageIndex,
pageSize,
selectedGroup,
onChangeGroupsItemsPerPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
Expand All @@ -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>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 5d83336

Please sign in to comment.