[Cloud Security][CNVM] Vulnerabilities Grouped by Resource page (#158987)

This commit is contained in:
Paulo Henrique 2023-06-14 08:59:44 -07:00 committed by GitHub
parent 723d120c1f
commit a549d52c21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1080 additions and 88 deletions

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { PostureTypes, VulnSeverity } from './types';
export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status';
@ -61,7 +60,6 @@ export const POSTURE_TYPE_ALL = 'all';
export const INTERNAL_FEATURE_FLAGS = {
showManageRulesMock: false,
showFindingFlyoutEvidence: false,
showFindingsGroupBy: true,
} as const;
export const CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE = 'csp-rule-template';
@ -106,29 +104,12 @@ export const POSTURE_TYPES: { [x: string]: PostureTypes } = {
export const VULNERABILITIES = 'vulnerabilities';
export const CONFIGURATIONS = 'configurations';
export const getSafeVulnerabilitiesQueryFilter = (query?: QueryDslQueryContainer) => ({
...query,
bool: {
...query?.bool,
filter: [
...((query?.bool?.filter as []) || []),
{ exists: { field: 'vulnerability.score.base' } },
{ exists: { field: 'vulnerability.score.version' } },
{ exists: { field: 'vulnerability.severity' } },
{ exists: { field: 'resource.name' } },
{ match_phrase: { 'vulnerability.enumeration': 'CVE' } },
],
must_not: [
...((query?.bool?.must_not as []) || []),
{ match_phrase: { 'vulnerability.severity': 'UNKNOWN' } },
],
},
});
export const SEVERITY: Record<VulnSeverity, VulnSeverity> = {
export const VULNERABILITIES_SEVERITY: Record<VulnSeverity, VulnSeverity> = {
LOW: 'LOW',
MEDIUM: 'MEDIUM',
HIGH: 'HIGH',
CRITICAL: 'CRITICAL',
UNKNOWN: 'UNKNOWN',
};
export const VULNERABILITIES_ENUMERATION = 'CVE';

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { VULNERABILITIES_ENUMERATION, VULNERABILITIES_SEVERITY } from '../constants';
export const getSafeVulnerabilitiesQueryFilter = (query?: QueryDslQueryContainer) => ({
...query,
bool: {
...query?.bool,
filter: [
...((query?.bool?.filter as []) || []),
{
bool: {
minimum_should_match: 1,
should: [
{ match_phrase: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.CRITICAL } },
{ match_phrase: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.HIGH } },
{ match_phrase: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.MEDIUM } },
{ match_phrase: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.LOW } },
],
},
},
{ exists: { field: 'vulnerability.score.base' } },
{ exists: { field: 'vulnerability.score.version' } },
{ exists: { field: 'vulnerability.severity' } },
{ exists: { field: 'resource.id' } },
{ exists: { field: 'resource.name' } },
{ match_phrase: { 'vulnerability.enumeration': VULNERABILITIES_ENUMERATION } },
],
},
});

View file

@ -89,6 +89,16 @@ export const findingsNavigation = {
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/findings/vulnerabilities`,
id: 'cloud_security_posture-findings-vulnerabilities',
},
vulnerabilities_by_resource: {
name: NAV_ITEMS_NAMES.FINDINGS,
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/findings/vulnerabilities/resource`,
id: 'cloud_security_posture-findings-vulnerabilities-resource',
},
resource_vulnerabilities: {
name: NAV_ITEMS_NAMES.FINDINGS,
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/findings/vulnerabilities/resource/:resourceId`,
id: 'cloud_security_posture-findings-vulnerabilities-resourceId',
},
};
const ELASTIC_BASE_SHORT_URL = 'https://ela.st';

View file

@ -6,6 +6,7 @@
*/
import { euiThemeVars } from '@kbn/ui-theme';
import { VULNERABILITIES_SEVERITY } from '../../../common/constants';
import { VulnSeverity } from '../../../common/types';
export const getCvsScoreColor = (score: number): string | undefined => {
@ -22,13 +23,13 @@ export const getCvsScoreColor = (score: number): string | undefined => {
export const getSeverityStatusColor = (severity: VulnSeverity): string => {
switch (severity) {
case 'LOW':
case VULNERABILITIES_SEVERITY.LOW:
return euiThemeVars.euiColorVis0;
case 'MEDIUM':
return euiThemeVars.euiColorVis7;
case 'HIGH':
return euiThemeVars.euiColorVis9;
case 'CRITICAL':
case VULNERABILITIES_SEVERITY.MEDIUM:
return euiThemeVars.euiColorVis5_behindText;
case VULNERABILITIES_SEVERITY.HIGH:
return euiThemeVars.euiColorVis9_behindText;
case VULNERABILITIES_SEVERITY.CRITICAL:
return euiThemeVars.euiColorDanger;
default:
return '#aaa';

View file

@ -36,11 +36,11 @@ describe('getSeverityStatusColor', () => {
});
it('should return the correct color for MEDIUM severity', () => {
expect(getSeverityStatusColor('MEDIUM')).toBe(euiThemeVars.euiColorVis7);
expect(getSeverityStatusColor('MEDIUM')).toBe(euiThemeVars.euiColorVis5_behindText);
});
it('should return the correct color for HIGH severity', () => {
expect(getSeverityStatusColor('HIGH')).toBe(euiThemeVars.euiColorVis9);
expect(getSeverityStatusColor('HIGH')).toBe(euiThemeVars.euiColorVis9_behindText);
});
it('should return the correct color for CRITICAL severity', () => {

View file

@ -140,11 +140,14 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => {
loading={findingsGroupByNone.isFetching}
/>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false} style={{ width: 400 }}>
{!error && <FindingsGroupBySelector type="default" />}
</EuiFlexItem>
</EuiFlexGroup>
{!error && (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false} style={{ width: 188 }}>
<FindingsGroupBySelector type="default" />
<EuiSpacer size="m" />
</EuiFlexItem>
</EuiFlexGroup>
)}
{error && <ErrorCallout error={error} />}
{!error && (
<>

View file

@ -112,11 +112,14 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => {
loading={findingsGroupByResource.isFetching}
/>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false} style={{ width: 400 }}>
{!error && <FindingsGroupBySelector type="resource" />}
</EuiFlexItem>
</EuiFlexGroup>
{!error && (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false} style={{ width: 188 }}>
<FindingsGroupBySelector type="resource" />
<EuiSpacer size="m" />
</EuiFlexItem>
</EuiFlexGroup>
)}
{error && <ErrorCallout error={error} />}
{!error && (
<>

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiComboBox, EuiFormLabel, EuiSpacer, type EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox, EuiFormLabel, type EuiComboBoxOptionOption } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants';
import type { FindingsGroupByKind } from '../../../common/types';
import { findingsNavigation } from '../../../common/navigation/constants';
import * as TEST_SUBJECTS from '../test_subjects';
@ -31,6 +30,7 @@ const getGroupByOptions = (): Array<EuiComboBoxOptionOption<FindingsGroupByKind>
interface Props {
type: FindingsGroupByKind;
pathnameHandler?: (opts: Array<EuiComboBoxOptionOption<FindingsGroupByKind>>) => string;
}
const getFindingsGroupPath = (opts: Array<EuiComboBoxOptionOption<FindingsGroupByKind>>) => {
@ -45,27 +45,27 @@ const getFindingsGroupPath = (opts: Array<EuiComboBoxOptionOption<FindingsGroupB
}
};
export const FindingsGroupBySelector = ({ type }: Props) => {
export const FindingsGroupBySelector = ({
type,
pathnameHandler = getFindingsGroupPath,
}: Props) => {
const groupByOptions = useMemo(getGroupByOptions, []);
const history = useHistory();
if (!INTERNAL_FEATURE_FLAGS.showFindingsGroupBy) return null;
const onChange = (options: Array<EuiComboBoxOptionOption<FindingsGroupByKind>>) =>
history.push({ pathname: getFindingsGroupPath(options) });
history.push({ pathname: pathnameHandler(options) });
return (
<div>
<EuiComboBox
data-test-subj={TEST_SUBJECTS.FINDINGS_GROUP_BY_SELECTOR}
prepend={<GroupByLabel />}
singleSelection={{ asPlainText: true }}
options={groupByOptions}
selectedOptions={groupByOptions.filter((o) => o.value === type)}
onChange={onChange}
/>
<EuiSpacer />
</div>
<EuiComboBox
data-test-subj={TEST_SUBJECTS.FINDINGS_GROUP_BY_SELECTOR}
prepend={<GroupByLabel />}
singleSelection={{ asPlainText: true }}
options={groupByOptions}
selectedOptions={groupByOptions.filter((o) => o.value === type)}
onChange={onChange}
isClearable={false}
compressed
/>
);
};

View file

@ -32,6 +32,15 @@ export const Findings = () => {
const navigateToConfigurationsTab = () => {
history.push({ pathname: findingsNavigation.findings_default.path });
};
const isVulnerabilitiesTabSelected = (pathname: string) => {
return (
pathname === findingsNavigation.vulnerabilities.path ||
pathname === findingsNavigation.vulnerabilities_by_resource.path ||
pathname === findingsNavigation.resource_vulnerabilities.path
);
};
return (
<>
<EuiTitle size="l">
@ -44,7 +53,7 @@ export const Findings = () => {
<EuiTab
key="vuln_mgmt"
onClick={navigateToVulnerabilitiesTab}
isSelected={location.pathname === findingsNavigation.vulnerabilities.path}
isSelected={isVulnerabilitiesTabSelected(location.pathname)}
>
<EuiFlexGroup responsive={false} alignItems="center" direction="row" gutterSize="s">
<EuiFlexItem grow={false}>
@ -73,7 +82,7 @@ export const Findings = () => {
<EuiTab
key="configurations"
onClick={navigateToConfigurationsTab}
isSelected={location.pathname !== findingsNavigation.vulnerabilities.path}
isSelected={!isVulnerabilitiesTabSelected(location.pathname)}
>
<FormattedMessage
id="xpack.csp.findings.tabs.misconfigurations"
@ -94,10 +103,14 @@ export const Findings = () => {
/>
)}
/>
<Route path={findingsNavigation.findings_default.path} component={Configurations} />
<Route path={findingsNavigation.vulnerabilities.path} component={Vulnerabilities} />
<Route path={findingsNavigation.findings_by_resource.path} component={Configurations} />
<Route path={findingsNavigation.vulnerabilities.path} component={Vulnerabilities} />
<Route
path={findingsNavigation.vulnerabilities_by_resource.path}
component={Vulnerabilities}
/>
{/* Redirect to default findings page if no match */}
<Route path="*" render={() => <Redirect to={findingsNavigation.findings_default.path} />} />
</Switch>
</>

View file

@ -8,23 +8,27 @@ import { useQuery } from '@tanstack/react-query';
import { lastValueFrom } from 'rxjs';
import type { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common';
import { number } from 'io-ts';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
getSafeVulnerabilitiesQueryFilter,
LATEST_VULNERABILITIES_INDEX_PATTERN,
} from '../../../../common/constants';
SearchRequest,
SearchResponse,
AggregationsMultiBucketAggregateBase,
AggregationsStringRareTermsBucketKeys,
Sort,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { LATEST_VULNERABILITIES_INDEX_PATTERN } from '../../../../common/constants';
import { getSafeVulnerabilitiesQueryFilter } from '../../../../common/utils/get_safe_vulnerabilities_query_filter';
import { useKibana } from '../../../common/hooks/use_kibana';
import { showErrorToast } from '../../../common/utils/show_error_toast';
import { FindingsBaseEsQuery } from '../../../common/types';
type LatestFindingsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<estypes.SearchResponse<any, FindingsAggs>>;
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<SearchResponse<any, FindingsAggs>>;
interface FindingsAggs {
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
}
interface VulnerabilitiesQuery extends FindingsBaseEsQuery {
sort: estypes.Sort;
sort: Sort;
enabled: boolean;
pageIndex: number;
pageSize: number;

View file

@ -0,0 +1,171 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { lastValueFrom } from 'rxjs';
import type { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common';
import {
SearchRequest,
SearchResponse,
AggregationsCardinalityAggregate,
AggregationsMultiBucketAggregateBase,
AggregationsSingleBucketAggregateBase,
AggregationsStringRareTermsBucketKeys,
AggregationsStringTermsBucketKeys,
SortOrder,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
LATEST_VULNERABILITIES_INDEX_PATTERN,
VULNERABILITIES_SEVERITY,
} from '../../../../common/constants';
import { getSafeVulnerabilitiesQueryFilter } from '../../../../common/utils/get_safe_vulnerabilities_query_filter';
import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants';
import { useKibana } from '../../../common/hooks/use_kibana';
import { showErrorToast } from '../../../common/utils/show_error_toast';
import { FindingsBaseEsQuery } from '../../../common/types';
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<SearchResponse<any, VulnerabilitiesAggs>>;
interface VulnerabilitiesAggs {
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
total: AggregationsCardinalityAggregate;
resources: AggregationsMultiBucketAggregateBase<FindingsAggBucket>;
}
interface FindingsAggBucket extends AggregationsStringRareTermsBucketKeys {
name: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>;
region: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>;
critical: AggregationsSingleBucketAggregateBase;
high: AggregationsSingleBucketAggregateBase;
medium: AggregationsSingleBucketAggregateBase;
low: AggregationsSingleBucketAggregateBase;
}
interface VulnerabilitiesQuery extends FindingsBaseEsQuery {
sortOrder: SortOrder;
enabled: boolean;
pageIndex: number;
pageSize: number;
}
export const getQuery = ({
query,
sortOrder = 'desc',
pageIndex,
pageSize,
}: VulnerabilitiesQuery) => ({
index: LATEST_VULNERABILITIES_INDEX_PATTERN,
query: getSafeVulnerabilitiesQueryFilter(query),
aggs: {
total: { cardinality: { field: 'resource.id' } },
resources: {
terms: {
field: 'resource.id',
size: MAX_FINDINGS_TO_LOAD * 3,
// in case there are more resources then size, ensuring resources with more vulnerabilities
// will be included first, and then vulnerabilities with critical and high severity
order: [{ _count: sortOrder }, { critical: 'desc' }, { high: 'desc' }, { medium: 'desc' }],
},
aggs: {
vulnerabilitiesCountBucketSort: {
bucket_sort: {
sort: [{ _count: { order: sortOrder } }],
from: pageIndex * pageSize,
size: pageSize,
},
},
name: {
terms: { field: 'resource.name', size: 1 },
},
region: {
terms: { field: 'cloud.region', size: 1 },
},
critical: {
filter: {
term: {
'vulnerability.severity': { value: VULNERABILITIES_SEVERITY.CRITICAL },
},
},
},
high: {
filter: {
term: {
'vulnerability.severity': { value: VULNERABILITIES_SEVERITY.HIGH },
},
},
},
medium: {
filter: {
term: {
'vulnerability.severity': { value: VULNERABILITIES_SEVERITY.MEDIUM },
},
},
},
low: {
filter: {
term: { 'vulnerability.severity': { value: VULNERABILITIES_SEVERITY.LOW } },
},
},
},
},
},
size: 0,
});
const getFirstKey = (
buckets: AggregationsMultiBucketAggregateBase<AggregationsStringTermsBucketKeys>['buckets']
): undefined | string => {
if (!!Array.isArray(buckets) && !!buckets.length) return buckets[0].key;
};
const createVulnerabilitiesByResource = (resource: FindingsAggBucket) => ({
'resource.id': resource.key,
'resource.name': getFirstKey(resource.name.buckets),
'cloud.region': getFirstKey(resource.region.buckets),
vulnerabilities_count: resource.doc_count,
severity_map: {
critical: resource.critical.doc_count,
high: resource.high.doc_count,
medium: resource.medium.doc_count,
low: resource.low.doc_count,
},
});
export const useLatestVulnerabilitiesByResource = (options: VulnerabilitiesQuery) => {
const {
data,
notifications: { toasts },
} = useKibana().services;
return useQuery(
[LATEST_VULNERABILITIES_INDEX_PATTERN, 'resource', options],
async () => {
const {
rawResponse: { hits, aggregations },
} = await lastValueFrom(
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({
params: getQuery(options),
})
);
if (!aggregations) throw new Error('Failed to aggregate by resource');
if (!Array.isArray(aggregations.resources.buckets))
throw new Error('Failed to group by, missing resource id');
return {
page: aggregations.resources.buckets.map(createVulnerabilitiesByResource),
total: aggregations.total.value,
total_vulnerabilities: hits.total as number,
};
},
{
staleTime: 5000,
keepPreviousData: true,
enabled: options.enabled,
onError: (err: Error) => showErrorToast(toasts, err),
}
);
};

View file

@ -24,6 +24,11 @@ export const useStyles = () => {
}
& .euiDataGrid__controls {
border-bottom: none;
margin-bottom: ${euiTheme.size.s};
& .euiButtonEmpty {
font-weight: ${euiTheme.font.weight.bold};
}
}
& .euiButtonIcon {
color: ${euiTheme.colors.primary};
@ -37,6 +42,9 @@ export const useStyles = () => {
& .euiDataGridRowCell__expandFlex {
align-items: center;
}
& .euiDataGridRowCell.euiDataGridRowCell--numeric {
text-align: left;
}
`;
const highlightStyle = css`
@ -46,8 +54,13 @@ export const useStyles = () => {
}
`;
const groupBySelector = css`
width: 188px;
`;
return {
highlightStyle,
gridStyle,
groupBySelector,
};
};

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { findingsNavigation } from '../../../common/navigation/constants';
import { FindingsGroupByKind } from '../../../common/types';
export const vulnerabilitiesPathnameHandler = (
opts: Array<EuiComboBoxOptionOption<FindingsGroupByKind>>
) => {
const [firstOption] = opts;
switch (firstOption?.value) {
case 'resource':
return findingsNavigation.vulnerabilities_by_resource.path;
case 'default':
default:
return findingsNavigation.vulnerabilities.path;
}
};

View file

@ -10,6 +10,7 @@ import {
EuiDataGrid,
EuiDataGridCellValueElementProps,
EuiDataGridColumnCellAction,
EuiFlexItem,
EuiProgress,
EuiSpacer,
EuiToolTip,
@ -19,6 +20,8 @@ import { cx } from '@emotion/css';
import { DataView } from '@kbn/data-views-plugin/common';
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { Switch } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../common/constants';
import { useCloudPostureTable } from '../../common/hooks/use_cloud_posture_table';
import { useLatestVulnerabilities } from './hooks/use_latest_vulnerabilities';
@ -47,6 +50,10 @@ import {
getCaseInsensitiveSortScript,
} from './utils/custom_sort_script';
import { useStyles } from './hooks/use_styles';
import { FindingsGroupBySelector } from '../configurations/layout/findings_group_by_selector';
import { vulnerabilitiesPathnameHandler } from './utils/vulnerabilities_pathname_handler';
import { findingsNavigation } from '../../common/navigation/constants';
import { VulnerabilitiesByResource } from './vulnerabilities_by_resource/vulnerabilities_by_resource';
const getDefaultQuery = ({ query, filters }: any): any => ({
query,
@ -75,7 +82,19 @@ export const Vulnerabilities = () => {
return defaultNoDataRenderer();
}
return <VulnerabilitiesContent dataView={data} />;
return (
<Switch>
<Route
exact
path={findingsNavigation.vulnerabilities_by_resource.path}
render={() => <VulnerabilitiesByResource dataView={data} />}
/>
<Route
path={findingsNavigation.vulnerabilities.path}
render={() => <VulnerabilitiesContent dataView={data} />}
/>
</Switch>
);
};
const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
@ -431,7 +450,7 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
loading={isLoading}
placeholder={SEARCH_BAR_PLACEHOLDER}
/>
<EuiSpacer size="l" />
<EuiSpacer size="m" />
{!isLoading && data.page.length === 0 ? (
<EmptyState onResetFilters={onResetFilters} />
) : (
@ -457,6 +476,7 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
showColumnSelector: false,
showDisplaySelector: false,
showKeyboardShortcuts: false,
showFullScreenSelector: false,
additionalControls: {
left: {
prepend: (
@ -471,6 +491,14 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
</>
),
},
right: (
<EuiFlexItem grow={false} className={styles.groupBySelector}>
<FindingsGroupBySelector
type="default"
pathnameHandler={vulnerabilitiesPathnameHandler}
/>
</EuiFlexItem>
),
},
}}
gridStyle={{

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const getVulnerabilitiesByResourceData = () => ({
total: 2,
total_vulnerabilities: 8,
page: [
{
'resource.id': 'resource-id-1',
'resource.name': 'resource-test-1',
'cloud.region': 'us-test-1',
vulnerabilities_count: 4,
severity_map: {
critical: 1,
high: 1,
medium: 1,
low: 1,
},
},
{
'resource.id': 'resource-id-2',
'resource.name': 'resource-test-2',
'cloud.region': 'us-test-1',
vulnerabilities_count: 4,
severity_map: {
critical: 1,
high: 1,
medium: 1,
low: 1,
},
},
],
});

View file

@ -0,0 +1,146 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { css } from '@emotion/css';
import {
EuiColorPaletteDisplay,
EuiToolTip,
useEuiTheme,
EuiFlexGroup,
EuiFlexItem,
EuiText,
} from '@elastic/eui';
import { ColorStop } from '@elastic/eui/src/components/color_picker/color_stops';
import { i18n } from '@kbn/i18n';
import { getSeverityStatusColor } from '../../../common/utils/get_vulnerability_colors';
import { VulnSeverity } from '../../../../common/types';
import { SeverityStatusBadge } from '../../../components/vulnerability_badges';
interface Props {
total: number;
severityMap: SeverityMap;
}
interface SeverityMap {
critical: number;
high: number;
medium: number;
low: number;
}
interface SeverityMapTooltip {
severity: string;
count: number;
percentage: number;
}
const formatPercentage = (percentage: number) => {
if (percentage === 0) {
return '0%';
}
if (percentage === 100) {
return '100%';
}
return `${percentage.toFixed(1)}%`;
};
export const SeverityMap = ({ severityMap, total }: Props) => {
const { euiTheme } = useEuiTheme();
const severityMapPallet: ColorStop[] = [];
const severityMapTooltip: SeverityMapTooltip[] = [];
if (total > 0) {
// Setting a minimum stop value of 8% the palette bar to avoid the color
// palette being too small to be visible
const minStop = Math.max(0.08 * total, 1);
const severityLevels: Array<keyof SeverityMap> = ['low', 'medium', 'high', 'critical'];
let currentStop = 0;
severityLevels.forEach((severity) => {
if (severityMap[severity] > 0) {
currentStop += Math.max(severityMap[severity], minStop);
severityMapPallet.push({
stop: currentStop,
color: getSeverityStatusColor(severity.toUpperCase() as VulnSeverity),
});
}
severityMapTooltip.push({
severity,
count: severityMap[severity],
percentage: (severityMap[severity] / total) * 100,
});
});
}
return (
<EuiToolTip
className={css`
width: 256px;
`}
anchorClassName={css`
height: ${euiTheme.size.xl};
display: flex;
align-items: center;
`}
position="left"
title={i18n.translate('xpack.csp.vulnerabilitiesByResource.severityMap.tooltipTitle', {
defaultMessage: 'Severity map',
})}
content={<TooltipBody severityMapTooltip={severityMapTooltip} />}
>
<EuiColorPaletteDisplay
type="fixed"
palette={severityMapPallet}
className={css`
width: 100%;
`}
/>
</EuiToolTip>
);
};
const TooltipBody = ({ severityMapTooltip }: { severityMapTooltip: SeverityMapTooltip[] }) => {
const { euiTheme } = useEuiTheme();
return (
<>
{severityMapTooltip.map((severity) => (
<EuiFlexGroup justifyContent="spaceBetween" key={severity.severity} alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s">
<SeverityStatusBadge severity={severity.severity.toUpperCase() as VulnSeverity} />
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s">{severity.count}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText
textAlign="right"
size="s"
className={css`
width: ${euiTheme.size.xxxl};
color: ${euiTheme.colors.mediumShade};
`}
>
{formatPercentage(severity.percentage)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
))}
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const VULNERABILITY_RESOURCE_COUNT = 'vulnerability_resource_count';

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, screen } from '@testing-library/react';
import { VulnerabilitiesByResource } from './vulnerabilities_by_resource';
import { TestProvider } from '../../../test/test_provider';
import { useLatestVulnerabilitiesByResource } from '../hooks/use_latest_vulnerabilities_by_resource';
import { VULNERABILITY_RESOURCE_COUNT } from './test_subjects';
import { getVulnerabilitiesByResourceData } from './__mocks__/vulnerabilities_by_resource.mock';
jest.mock('../hooks/use_latest_vulnerabilities_by_resource', () => ({
useLatestVulnerabilitiesByResource: jest.fn(),
}));
beforeEach(() => {
jest.clearAllMocks();
});
describe('VulnerabilitiesByResource', () => {
const dataView: any = {};
const renderVulnerabilityByResource = () => {
return render(
<TestProvider>
<VulnerabilitiesByResource dataView={dataView} />
</TestProvider>
);
};
it('renders the loading state', () => {
(useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({
data: undefined,
isLoading: true,
isFetching: true,
});
renderVulnerabilityByResource();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('renders the no data state', () => {
(useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({
data: undefined,
isLoading: false,
isFetching: false,
});
renderVulnerabilityByResource();
expect(screen.getByText(/no data/i)).toBeInTheDocument();
});
it('renders the empty state component', () => {
(useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({
data: { total: 0, total_vulnerabilities: 0, page: [] },
isLoading: false,
isFetching: false,
});
renderVulnerabilityByResource();
expect(screen.getByText(/no results/i)).toBeInTheDocument();
});
it('renders the Table', () => {
(useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({
data: getVulnerabilitiesByResourceData(),
isLoading: false,
isFetching: false,
});
renderVulnerabilityByResource();
expect(screen.getByText(/2 resources/i)).toBeInTheDocument();
expect(screen.getByText(/8 vulnerabilities/i)).toBeInTheDocument();
expect(screen.getByText(/resource-id-1/i)).toBeInTheDocument();
expect(screen.getByText(/resource-id-2/i)).toBeInTheDocument();
expect(screen.getByText(/resource-test-1/i)).toBeInTheDocument();
expect(screen.getAllByText(/us-test-1/i)).toHaveLength(2);
expect(screen.getAllByTestId(VULNERABILITY_RESOURCE_COUNT)).toHaveLength(2);
expect(screen.getAllByTestId(VULNERABILITY_RESOURCE_COUNT)[0]).toHaveTextContent('4');
expect(screen.getAllByTestId(VULNERABILITY_RESOURCE_COUNT)[1]).toHaveTextContent('4');
});
});

View file

@ -0,0 +1,345 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiBadge,
EuiButtonEmpty,
EuiDataGrid,
EuiDataGridCellValueElementProps,
EuiDataGridColumnCellAction,
EuiFlexItem,
EuiProgress,
EuiSpacer,
EuiToolTip,
} from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../common/constants';
import { useCloudPostureTable } from '../../../common/hooks/use_cloud_posture_table';
import { ErrorCallout } from '../../configurations/layout/error_callout';
import { FindingsSearchBar } from '../../configurations/layout/findings_search_bar';
import { useLimitProperties } from '../../../common/utils/get_limit_properties';
import { LimitedResultsBar } from '../../configurations/layout/findings_layout';
import {
getVulnerabilitiesByResourceColumnsGrid,
vulnerabilitiesByResourceColumns,
} from './vulnerabilities_by_resource_table_columns';
import {
defaultLoadingRenderer,
defaultNoDataRenderer,
} from '../../../components/cloud_posture_page';
import { getFilters } from '../utils/get_filters';
import { FILTER_IN, FILTER_OUT, SEARCH_BAR_PLACEHOLDER, VULNERABILITIES } from '../translations';
import { useStyles } from '../hooks/use_styles';
import { FindingsGroupBySelector } from '../../configurations/layout/findings_group_by_selector';
import { vulnerabilitiesPathnameHandler } from '../utils/vulnerabilities_pathname_handler';
import { useLatestVulnerabilitiesByResource } from '../hooks/use_latest_vulnerabilities_by_resource';
import { EmptyState } from '../../../components/empty_state';
import { SeverityMap } from './severity_map';
import { VULNERABILITY_RESOURCE_COUNT } from './test_subjects';
const getDefaultQuery = ({ query, filters }: any): any => ({
query,
filters,
sort: [{ id: vulnerabilitiesByResourceColumns.vulnerabilities_count, direction: 'desc' }],
pageIndex: 0,
});
export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView }) => {
const {
pageIndex,
query,
sort,
queryError,
pageSize,
onChangeItemsPerPage,
onChangePage,
onSort,
urlQuery,
setUrlQuery,
onResetFilters,
} = useCloudPostureTable({
dataView,
defaultQuery: getDefaultQuery,
paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY,
});
const styles = useStyles();
const { data, isLoading, isFetching } = useLatestVulnerabilitiesByResource({
query,
sortOrder: sort[0]?.direction,
enabled: !queryError,
pageIndex,
pageSize,
});
const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({
total: data?.total,
pageIndex,
pageSize,
});
const columns = useMemo(() => {
const getColumnIdValue = (rowIndex: number, columnId: string) => {
const vulnerabilityRow = data?.page[rowIndex];
if (!vulnerabilityRow) return null;
if (columnId === vulnerabilitiesByResourceColumns.resource_id) {
return vulnerabilityRow['resource.id'];
}
if (columnId === vulnerabilitiesByResourceColumns.resource_name) {
return vulnerabilityRow['resource.name'];
}
if (columnId === vulnerabilitiesByResourceColumns.region) {
return vulnerabilityRow['cloud.region'];
}
};
const cellActions: EuiDataGridColumnCellAction[] = [
({ Component, rowIndex, columnId }) => {
const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex;
const value = getColumnIdValue(rowIndexFromPage, columnId);
if (!value) return null;
return (
<EuiToolTip
position="top"
content={i18n.translate(
'xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButtonTooltip',
{
defaultMessage: 'Add {columnId} filter',
values: { columnId },
}
)}
>
<Component
iconType="plusInCircle"
aria-label={i18n.translate(
'xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButton',
{
defaultMessage: 'Add {columnId} negated filter',
values: { columnId },
}
)}
onClick={() => {
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field: columnId,
value,
negate: false,
}),
});
}}
>
{FILTER_IN}
</Component>
</EuiToolTip>
);
},
({ Component, rowIndex, columnId }) => {
const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex;
const value = getColumnIdValue(rowIndexFromPage, columnId);
if (!value) return null;
return (
<EuiToolTip
position="top"
content={i18n.translate(
'xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegatedFilterButtonTooltip',
{
defaultMessage: 'Add {columnId} negated filter',
values: { columnId },
}
)}
>
<Component
iconType="minusInCircle"
aria-label={i18n.translate(
'xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegateFilterButton',
{
defaultMessage: 'Add {columnId} negated filter',
values: { columnId },
}
)}
onClick={() => {
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field: columnId,
value,
negate: true,
}),
});
}}
>
{FILTER_OUT}
</Component>
</EuiToolTip>
);
},
];
return getVulnerabilitiesByResourceColumnsGrid(cellActions);
}, [data?.page, dataView, pageSize, setUrlQuery, urlQuery.filters]);
const renderCellValue = useMemo(() => {
const Cell: React.FC<EuiDataGridCellValueElementProps> = ({
columnId,
rowIndex,
}): React.ReactElement | null => {
const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex;
const resourceVulnerabilityRow = data?.page[rowIndexFromPage];
if (isFetching) return null;
if (!resourceVulnerabilityRow?.['resource.id']) return null;
if (columnId === vulnerabilitiesByResourceColumns.resource_id) {
return <>{resourceVulnerabilityRow['resource.id']}</>;
}
if (columnId === vulnerabilitiesByResourceColumns.resource_name) {
return <>{resourceVulnerabilityRow['resource.name']}</>;
}
if (columnId === vulnerabilitiesByResourceColumns.region) {
return <>{resourceVulnerabilityRow['cloud.region']}</>;
}
if (columnId === vulnerabilitiesByResourceColumns.vulnerabilities_count) {
return (
<EuiBadge color="hollow" data-test-subj={VULNERABILITY_RESOURCE_COUNT}>
{resourceVulnerabilityRow.vulnerabilities_count}
</EuiBadge>
);
}
if (columnId === vulnerabilitiesByResourceColumns.severity_map) {
return (
<SeverityMap
total={resourceVulnerabilityRow.vulnerabilities_count}
severityMap={resourceVulnerabilityRow.severity_map}
/>
);
}
return null;
};
return Cell;
}, [data?.page, pageSize, isFetching]);
const error = queryError || null;
if (error) {
return <ErrorCallout error={error as Error} />;
}
if (isLoading) {
return defaultLoadingRenderer();
}
if (!data?.page) {
return defaultNoDataRenderer();
}
return (
<>
<FindingsSearchBar
dataView={dataView}
setQuery={(newQuery) => {
setUrlQuery({ ...newQuery, pageIndex: 0 });
}}
loading={isLoading}
placeholder={SEARCH_BAR_PLACEHOLDER}
/>
<EuiSpacer size="m" />
{!isLoading && data.page.length === 0 ? (
<EmptyState onResetFilters={onResetFilters} />
) : (
<>
<EuiProgress
size="xs"
color="accent"
style={{
opacity: isFetching ? 1 : 0,
}}
/>
<EuiDataGrid
className={styles.gridStyle}
aria-label={VULNERABILITIES}
columns={columns}
columnVisibility={{
visibleColumns: columns.map(({ id }) => id),
setVisibleColumns: () => {},
}}
rowCount={limitedTotalItemCount}
toolbarVisibility={{
showColumnSelector: false,
showDisplaySelector: false,
showKeyboardShortcuts: false,
showSortSelector: false,
showFullScreenSelector: false,
additionalControls: {
left: {
prepend: (
<>
<EuiButtonEmpty size="xs" color="text">
{i18n.translate('xpack.csp.vulnerabilitiesByResource.totalResources', {
defaultMessage: '{total, plural, one {# Resource} other {# Resources}}',
values: { total: data?.total },
})}
</EuiButtonEmpty>
<EuiButtonEmpty size="xs" color="text">
{i18n.translate(
'xpack.csp.vulnerabilitiesByResource.totalVulnerabilities',
{
defaultMessage:
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
values: { total: data?.total_vulnerabilities },
}
)}
</EuiButtonEmpty>
</>
),
},
right: (
<EuiFlexItem grow={false} className={styles.groupBySelector}>
<FindingsGroupBySelector
type="resource"
pathnameHandler={vulnerabilitiesPathnameHandler}
/>
</EuiFlexItem>
),
},
}}
gridStyle={{
border: 'horizontal',
cellPadding: 'l',
stripes: false,
rowHover: 'none',
header: 'underline',
}}
renderCellValue={renderCellValue}
inMemory={{ level: 'enhancements' }}
sorting={{ columns: sort, onSort }}
pagination={{
pageIndex,
pageSize,
pageSizeOptions: [10, 25, 100],
onChangeItemsPerPage,
onChangePage,
}}
/>
{isLastLimitedPage && <LimitedResultsBar />}
</>
)}
</>
);
};

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiDataGridColumn, EuiDataGridColumnCellAction } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const vulnerabilitiesByResourceColumns = {
resource_id: 'resource.id',
resource_name: 'resource.name',
region: 'cloud.region',
vulnerabilities_count: 'vulnerabilities_count',
severity_map: 'severity_map',
};
const defaultColumnProps = (): Partial<EuiDataGridColumn> => ({
isExpandable: false,
actions: {
showHide: false,
showMoveLeft: false,
showMoveRight: false,
showSortAsc: false,
showSortDesc: false,
},
isSortable: false,
});
export const getVulnerabilitiesByResourceColumnsGrid = (
cellActions: EuiDataGridColumnCellAction[]
): EuiDataGridColumn[] => [
{
...defaultColumnProps(),
id: vulnerabilitiesByResourceColumns.resource_id,
displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceId', {
defaultMessage: 'Resource ID',
}),
cellActions,
},
{
...defaultColumnProps(),
id: vulnerabilitiesByResourceColumns.resource_name,
displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceName', {
defaultMessage: 'Resource Name',
}),
cellActions,
},
{
...defaultColumnProps(),
id: vulnerabilitiesByResourceColumns.region,
displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.region', {
defaultMessage: 'Region',
}),
cellActions,
initialWidth: 150,
},
{
...defaultColumnProps(),
actions: {
showHide: false,
showMoveLeft: false,
showMoveRight: false,
showSortAsc: true,
showSortDesc: true,
},
id: vulnerabilitiesByResourceColumns.vulnerabilities_count,
displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.vulnerabilities', {
defaultMessage: 'Vulnerabilities',
}),
initialWidth: 140,
isResizable: false,
isSortable: true,
},
{
...defaultColumnProps(),
id: vulnerabilitiesByResourceColumns.severity_map,
displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.severityMap', {
defaultMessage: 'Severity Map',
}),
cellActions,
initialWidth: 110,
isResizable: false,
},
];

View file

@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { NvdLogo } from '../../../assets/icons/nvd_logo_svg';
import { CVSScoreBadge } from '../../../components/vulnerability_badges';
import { CVSScoreProps, VectorScoreBase, Vendor, Vulnerability } from '../types';
import { getVectorScoreList } from '../utils';
import { getVectorScoreList } from '../utils/get_vector_score_list';
import { OVERVIEW_TAB_VULNERABILITY_FLYOUT } from '../test_subjects';
import redhatLogo from '../../../assets/icons/redhat_logo.svg';

View file

@ -7,7 +7,7 @@
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SEVERITY } from '../../../common/constants';
import { VULNERABILITIES_SEVERITY } from '../../../common/constants';
import { useCnvmStatisticsApi } from '../../common/api/use_vulnerabilities_stats_api';
import { useNavigateVulnerabilities } from '../../common/hooks/use_navigate_findings';
import { CompactFormattedNumber } from '../../components/compact_formatted_number';
@ -40,42 +40,42 @@ export const VulnerabilityStatistics = () => {
id: 'critical-count-stat',
title: <CompactFormattedNumber number={getCnvmStats.data?.cnvmStatistics.criticalCount} />,
description: (
<EuiHealth color={getSeverityStatusColor(SEVERITY.CRITICAL)}>
<EuiHealth color={getSeverityStatusColor(VULNERABILITIES_SEVERITY.CRITICAL)}>
{i18n.translate('xpack.csp.cnvmDashboard.statistics.criticalTitle', {
defaultMessage: 'Critical',
})}
</EuiHealth>
),
onClick: () => {
navToVulnerabilities({ 'vulnerability.severity': SEVERITY.CRITICAL });
navToVulnerabilities({ 'vulnerability.severity': VULNERABILITIES_SEVERITY.CRITICAL });
},
},
{
id: 'high-count-stat',
title: <CompactFormattedNumber number={getCnvmStats.data?.cnvmStatistics.highCount} />,
description: (
<EuiHealth color={getSeverityStatusColor(SEVERITY.HIGH)}>
<EuiHealth color={getSeverityStatusColor(VULNERABILITIES_SEVERITY.HIGH)}>
{i18n.translate('xpack.csp.cnvmDashboard.statistics.highTitle', {
defaultMessage: 'High',
})}
</EuiHealth>
),
onClick: () => {
navToVulnerabilities({ 'vulnerability.severity': SEVERITY.HIGH });
navToVulnerabilities({ 'vulnerability.severity': VULNERABILITIES_SEVERITY.HIGH });
},
},
{
id: 'medium-count-stat',
title: <CompactFormattedNumber number={getCnvmStats.data?.cnvmStatistics.mediumCount} />,
description: (
<EuiHealth color={getSeverityStatusColor(SEVERITY.MEDIUM)}>
<EuiHealth color={getSeverityStatusColor(VULNERABILITIES_SEVERITY.MEDIUM)}>
{i18n.translate('xpack.csp.cnvmDashboard.statistics.mediumTitle', {
defaultMessage: 'Medium',
})}
</EuiHealth>
),
onClick: () => {
navToVulnerabilities({ 'vulnerability.severity': SEVERITY.MEDIUM });
navToVulnerabilities({ 'vulnerability.severity': VULNERABILITIES_SEVERITY.MEDIUM });
},
},
],
@ -85,7 +85,7 @@ export const VulnerabilityStatistics = () => {
return (
<EuiFlexGroup>
{stats.map((stat) => (
<EuiFlexItem>
<EuiFlexItem key={stat.id}>
<CspCounterCard {...stat} />
</EuiFlexItem>
))}

View file

@ -7,7 +7,10 @@
import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { LATEST_VULNERABILITIES_INDEX_DEFAULT_NS } from '../../../common/constants';
import {
LATEST_VULNERABILITIES_INDEX_DEFAULT_NS,
VULNERABILITIES_SEVERITY,
} from '../../../common/constants';
export interface VulnerabilitiesStatisticsQueryResult {
critical: {
@ -35,13 +38,13 @@ export const getVulnerabilitiesStatisticsQuery = (
index: LATEST_VULNERABILITIES_INDEX_DEFAULT_NS,
aggs: {
critical: {
filter: { term: { 'vulnerability.severity': 'CRITICAL' } },
filter: { term: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.CRITICAL } },
},
high: {
filter: { term: { 'vulnerability.severity': 'HIGH' } },
filter: { term: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.HIGH } },
},
medium: {
filter: { term: { 'vulnerability.severity': 'MEDIUM' } },
filter: { term: { 'vulnerability.severity': VULNERABILITIES_SEVERITY.MEDIUM } },
},
resources_scanned: {
cardinality: {

View file

@ -7,10 +7,8 @@
import { transformError } from '@kbn/securitysolution-es-utils';
import type { CnvmDashboardData } from '../../../common/types';
import {
VULNERABILITIES_DASHBOARD_ROUTE_PATH,
getSafeVulnerabilitiesQueryFilter,
} from '../../../common/constants';
import { VULNERABILITIES_DASHBOARD_ROUTE_PATH } from '../../../common/constants';
import { getSafeVulnerabilitiesQueryFilter } from '../../../common/utils/get_safe_vulnerabilities_query_filter';
import { CspRouter } from '../../types';
import { getVulnerabilitiesStatistics } from './get_vulnerabilities_statistics';