[Cloud Security]Vulnerabilities table in Contextual flyout (#195143)

## Summary

This PR is for Vulnerabilities data table in contextual flyout
It also addresses the ticket to remove Empty State for Preview Component
[ticket](https://github.com/elastic/security-team/issues/10746)
<img width="1510" alt="Screenshot 2024-10-07 at 2 14 52 AM"
src="https://github.com/user-attachments/assets/3c4cdc86-68c6-439c-96a1-92cece88e42e">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Maxim Kholod <maxim.kholod@elastic.co>
This commit is contained in:
Rickyanto Ang 2024-10-09 20:25:28 +07:00 committed by GitHub
parent fbf3f8b8b2
commit 59f2f85b8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1007 additions and 330 deletions

View file

@ -22,6 +22,7 @@ export const VULNERABILITIES_FLYOUT_VISITS = 'vulnerabilities-flyout-visits';
export const OPEN_FINDINGS_FLYOUT = 'open-findings-flyout';
export const GROUP_BY_CLICK = 'group-by-click';
export const CHANGE_RULE_STATE = 'change-rule-state';
export const ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS = 'entity-flyout-vulnerability-view-visits';
type CloudSecurityUiCounters =
| typeof ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS
@ -32,6 +33,7 @@ type CloudSecurityUiCounters =
| typeof CREATE_DETECTION_RULE_FROM_FLYOUT
| typeof CREATE_DETECTION_FROM_TABLE_ROW_ACTION
| typeof GROUP_BY_CLICK
| typeof ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS
| typeof CHANGE_RULE_STATE;
export class UiMetricService {

View file

@ -12,5 +12,7 @@ export type { NavFilter } from './src/hooks/use_navigate_findings';
export { showErrorToast } from './src/utils/show_error_toast';
export { encodeQuery, decodeQuery } from './src/utils/query_utils';
export { CspEvaluationBadge } from './src/components/csp_evaluation_badge';
export { getSeverityStatusColor } from './src/utils/get_vulnerability_colors';
export { getSeverityStatusColor, getCvsScoreColor } from './src/utils/get_vulnerability_colors';
export { getSeverityText } from './src/utils/get_vulnerability_text';
export { getVulnerabilityStats, hasVulnerabilitiesData } from './src/utils/vulnerability_helpers';
export { CVSScoreBadge, SeverityStatusBadge } from './src/components/vulnerability_badges';

View file

@ -10,9 +10,9 @@ import React from 'react';
import { css } from '@emotion/react';
import { float } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { VulnSeverity } from '@kbn/cloud-security-posture-common';
import { getSeverityStatusColor } from '@kbn/cloud-security-posture';
import { getCvsScoreColor } from '../common/utils/get_vulnerability_colors';
import { VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ } from './test_subjects';
import { getCvsScoreColor, getSeverityStatusColor } from '../utils/get_vulnerability_colors';
const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score_badge';
interface CVSScoreBadgeProps {
score?: float;

View file

@ -38,7 +38,7 @@ export const useMisconfigurationPreview = (options: UseCspOptions) => {
params: buildMisconfigurationsFindingsQuery(options, rulesStates!),
})
);
if (!aggregations && !options.ignore_unavailable)
if (!aggregations && options.ignore_unavailable === false)
throw new Error('expected aggregations to be defined');
return {
count: getMisconfigurationAggregationCount(aggregations?.count?.buckets),

View file

@ -0,0 +1,68 @@
/*
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
import { lastValueFrom } from 'rxjs';
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import {
SearchRequest,
SearchResponse,
AggregationsMultiBucketAggregateBase,
AggregationsStringRareTermsBucketKeys,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import type { CoreStart } from '@kbn/core/public';
import type { CspClientPluginStartDeps, UseCspOptions } from '../../type';
import { showErrorToast } from '../..';
import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils';
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<
SearchResponse<CspVulnerabilityFinding, FindingsAggs>
>;
interface FindingsAggs {
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
}
export const useVulnerabilitiesFindings = (options: UseCspOptions) => {
const {
data,
notifications: { toasts },
} = useKibana<CoreStart & CspClientPluginStartDeps>().services;
/**
* We're using useInfiniteQuery in this case to allow the user to fetch more data (if available and up to 10k)
* useInfiniteQuery differs from useQuery because it accumulates and caches a chunk of data from the previous fetches into an array
* it uses the getNextPageParam to know if there are more pages to load and retrieve the position of
* the last loaded record to be used as a from parameter to fetch the next chunk of data.
*/
return useQuery(
['csp_vulnerabilities_findings', { params: options }],
async ({ pageParam }) => {
const {
rawResponse: { aggregations, hits },
} = await lastValueFrom(
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({
params: getVulnerabilitiesQuery(options, pageParam),
})
);
return {
count: getVulnerabilitiesAggregationCount(aggregations?.count?.buckets),
rows: hits.hits.map((finding) => ({
vulnerability: finding._source?.vulnerability,
resource: finding._source?.resource,
})) as Array<Pick<CspVulnerabilityFinding, 'vulnerability' | 'resource'>>,
};
},
{
keepPreviousData: true,
enabled: options.enabled,
onError: (err: Error) => showErrorToast(toasts, err),
}
);
};

View file

@ -14,18 +14,11 @@ import {
AggregationsMultiBucketAggregateBase,
AggregationsStringRareTermsBucketKeys,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
CDR_VULNERABILITIES_INDEX_PATTERN,
LATEST_VULNERABILITIES_RETENTION_POLICY,
} from '@kbn/cloud-security-posture-common';
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import type { CoreStart } from '@kbn/core/public';
import type { CspClientPluginStartDeps, UseCspOptions } from '../../type';
import { showErrorToast } from '../..';
import {
getFindingsCountAggQueryVulnerabilities,
getVulnerabilitiesAggregationCount,
} from '../utils/hooks_utils';
import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils';
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<
@ -36,30 +29,6 @@ interface FindingsAggs {
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
}
const getVulnerabilitiesQuery = ({ query }: UseCspOptions, isPreview = false) => ({
index: CDR_VULNERABILITIES_INDEX_PATTERN,
size: 0,
aggs: getFindingsCountAggQueryVulnerabilities(),
ignore_unavailable: true,
query: {
...query,
bool: {
...query?.bool,
filter: [
...(query?.bool?.filter ?? []),
{
range: {
'@timestamp': {
gte: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`,
lte: 'now',
},
},
},
],
},
},
});
export const useVulnerabilitiesPreview = (options: UseCspOptions) => {
const {
data,
@ -73,7 +42,7 @@ export const useVulnerabilitiesPreview = (options: UseCspOptions) => {
rawResponse: { aggregations },
} = await lastValueFrom(
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({
params: getVulnerabilitiesQuery(options),
params: getVulnerabilitiesQuery(options, true),
})
);

View file

@ -6,7 +6,7 @@
*/
import { euiThemeVars } from '@kbn/ui-theme';
import { getSeverityStatusColor } from './get_vulnerability_colors';
import { getCvsScoreColor, getSeverityStatusColor } from './get_vulnerability_colors';
describe('getSeverityStatusColor', () => {
it('should return the correct color for LOW severity', () => {
expect(getSeverityStatusColor('LOW')).toBe(euiThemeVars.euiColorVis0);
@ -28,3 +28,25 @@ describe('getSeverityStatusColor', () => {
expect(getSeverityStatusColor('UNKNOWN')).toBe('#aaa');
});
});
describe('getCvsScoreColor', () => {
it('returns correct color for low severity score', () => {
expect(getCvsScoreColor(1.5)).toBe(euiThemeVars.euiColorVis0);
});
it('returns correct color for medium severity score', () => {
expect(getCvsScoreColor(5.5)).toBe(euiThemeVars.euiColorVis7);
});
it('returns correct color for high severity score', () => {
expect(getCvsScoreColor(7.9)).toBe(euiThemeVars.euiColorVis9);
});
it('returns correct color for critical severity score', () => {
expect(getCvsScoreColor(10.0)).toBe(euiThemeVars.euiColorDanger);
});
it('returns correct color for low severity score for undefined value', () => {
expect(getCvsScoreColor(-0.2)).toBe(euiThemeVars.euiColorVis0);
});
});

View file

@ -9,6 +9,18 @@ import { euiThemeVars } from '@kbn/ui-theme';
import type { VulnSeverity } from '@kbn/cloud-security-posture-common';
import { VULNERABILITIES_SEVERITY } from '@kbn/cloud-security-posture-common';
export const getCvsScoreColor = (score: number): string | undefined => {
if (score <= 4) {
return euiThemeVars.euiColorVis0; // low severity
} else if (score >= 4 && score <= 7) {
return euiThemeVars.euiColorVis7; // medium severity
} else if (score >= 7 && score <= 9) {
return euiThemeVars.euiColorVis9; // high severity
} else if (score >= 9) {
return euiThemeVars.euiColorDanger; // critical severity
}
};
export const getSeverityStatusColor = (severity: VulnSeverity): string => {
switch (severity) {
case VULNERABILITIES_SEVERITY.LOW:

View file

@ -8,7 +8,9 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
CDR_MISCONFIGURATIONS_INDEX_PATTERN,
CDR_VULNERABILITIES_INDEX_PATTERN,
LATEST_FINDINGS_RETENTION_POLICY,
LATEST_VULNERABILITIES_RETENTION_POLICY,
} from '@kbn/cloud-security-posture-common';
import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest';
import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common';
@ -161,3 +163,31 @@ export const getFindingsCountAggQueryVulnerabilities = () => ({
},
},
});
export const getVulnerabilitiesQuery = ({ query }: UseCspOptions, isPreview = false) => ({
index: CDR_VULNERABILITIES_INDEX_PATTERN,
size: isPreview ? 0 : 500,
aggs: getFindingsCountAggQueryVulnerabilities(),
ignore_unavailable: true,
query: buildVulnerabilityFindingsQueryWithFilters(query),
});
const buildVulnerabilityFindingsQueryWithFilters = (query: UseCspOptions['query']) => {
return {
...query,
bool: {
...query?.bool,
filter: [
...(query?.bool?.filter ?? []),
{
range: {
'@timestamp': {
gte: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`,
lte: 'now',
},
},
},
],
},
};
};

View file

@ -0,0 +1,74 @@
/*
* 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 { euiThemeVars } from '@kbn/ui-theme';
import { getVulnerabilityStats } from './vulnerability_helpers';
import { i18n } from '@kbn/i18n';
describe('getVulnerabilitiesAggregationCount', () => {
it('should return empty array when all severity count is 0', () => {
const result = getVulnerabilityStats({ critical: 0, high: 0, medium: 0, low: 0, none: 0 });
expect(result).toEqual([]);
});
it('should return stats for low, medium, high, and critical vulnerabilities', () => {
const result = getVulnerabilityStats({ critical: 1, high: 2, medium: 3, low: 4, none: 5 });
expect(result).toEqual([
{
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText',
{
defaultMessage: 'None',
}
),
count: 5,
color: '#aaa',
},
{
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.lowVulnerabilitiesText',
{
defaultMessage: 'Low',
}
),
count: 4,
color: euiThemeVars.euiColorVis0,
},
{
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.mediumVulnerabilitiesText',
{
defaultMessage: 'Medium',
}
),
count: 3,
color: euiThemeVars.euiColorVis5_behindText,
},
{
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.highVulnerabilitiesText',
{
defaultMessage: 'High',
}
),
count: 2,
color: euiThemeVars.euiColorVis9_behindText,
},
{
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.CriticalVulnerabilitiesText',
{
defaultMessage: 'Critical',
}
),
count: 1,
color: euiThemeVars.euiColorDanger,
},
]);
});
});

View file

@ -0,0 +1,101 @@
/*
* 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 { VULNERABILITIES_SEVERITY } from '@kbn/cloud-security-posture-common';
import { i18n } from '@kbn/i18n';
import { getSeverityStatusColor } from './get_vulnerability_colors';
import { getSeverityText } from './get_vulnerability_text';
interface VulnerabilitiesDistributionBarProps {
key: string;
count: number;
color: string;
}
interface VulnerabilityCounts {
critical: number;
high: number;
medium: number;
low: number;
none: number;
}
export const hasVulnerabilitiesData = (counts: VulnerabilityCounts): boolean => {
if (Object.values(counts).reduce((acc, value) => acc + value, 0) > 0) return true;
return false;
};
export const getVulnerabilityStats = (
counts: VulnerabilityCounts
): VulnerabilitiesDistributionBarProps[] => {
const vulnerabilityStats: VulnerabilitiesDistributionBarProps[] = [];
const levels = Object.values(counts);
if (levels.every((level) => level === 0)) {
return vulnerabilityStats;
}
if (counts.none > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.UNKNOWN),
}
),
count: counts.none,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.UNKNOWN),
});
if (counts.low > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.lowVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.LOW),
}
),
count: counts.low,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.LOW),
});
if (counts.medium > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.mediumVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.MEDIUM),
}
),
count: counts.medium,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.MEDIUM),
});
if (counts.high > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.highVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.HIGH),
}
),
count: counts.high,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.HIGH),
});
if (counts.critical > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.CriticalVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.CRITICAL),
}
),
count: counts.critical,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.CRITICAL),
});
return vulnerabilityStats;
};

View file

@ -5,7 +5,8 @@
"types": [
"jest",
"node",
"react"
"react",
"@emotion/react/types/css-prop"
]
},
"include": [

View file

@ -1,20 +0,0 @@
/*
* 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 { euiThemeVars } from '@kbn/ui-theme';
export const getCvsScoreColor = (score: number): string | undefined => {
if (score <= 4) {
return euiThemeVars.euiColorVis0; // low severity
} else if (score >= 4 && score <= 7) {
return euiThemeVars.euiColorVis7; // medium severity
} else if (score >= 7 && score <= 9) {
return euiThemeVars.euiColorVis9; // high severity
} else if (score >= 9) {
return euiThemeVars.euiColorDanger; // critical severity
}
};

View file

@ -1,31 +0,0 @@
/*
* 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 { euiThemeVars } from '@kbn/ui-theme';
import { getCvsScoreColor } from './get_vulnerability_colors';
describe('getCvsScoreColor', () => {
it('returns correct color for low severity score', () => {
expect(getCvsScoreColor(1.5)).toBe(euiThemeVars.euiColorVis0);
});
it('returns correct color for medium severity score', () => {
expect(getCvsScoreColor(5.5)).toBe(euiThemeVars.euiColorVis7);
});
it('returns correct color for high severity score', () => {
expect(getCvsScoreColor(7.9)).toBe(euiThemeVars.euiColorVis9);
});
it('returns correct color for critical severity score', () => {
expect(getCvsScoreColor(10.0)).toBe(euiThemeVars.euiColorDanger);
});
it('returns correct color for low severity score for undefined value', () => {
expect(getCvsScoreColor(-0.2)).toBe(euiThemeVars.euiColorVis0);
});
});

View file

@ -42,8 +42,6 @@ export const THIRD_PARTY_NO_VULNERABILITIES_FINDINGS_PROMPT_WIZ_INTEGRATION_BUTT
'3p-no-vulnerabilities-findings-prompt-wiz-integration-button';
export const VULNERABILITIES_CONTAINER_TEST_SUBJ = 'vulnerabilities_container';
export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score_badge';
export const TAKE_ACTION_SUBJ = 'csp:take_action';
export const CREATE_RULE_ACTION_SUBJ = 'csp:create_rule';

View file

@ -18,7 +18,7 @@ import { PaletteColorStop } from '@elastic/eui/src/components/color_picker/color
import type { VulnSeverity } from '@kbn/cloud-security-posture-common';
import { i18n } from '@kbn/i18n';
import { getSeverityStatusColor } from '@kbn/cloud-security-posture';
import { SeverityStatusBadge } from './vulnerability_badges';
import { SeverityStatusBadge } from '@kbn/cloud-security-posture';
interface Props {
total: number;

View file

@ -12,6 +12,7 @@ import { EuiDataGridCellValueElementProps, EuiSpacer } from '@elastic/eui';
import { Filter } from '@kbn/es-query';
import { HttpSetup } from '@kbn/core-http-browser';
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import { CVSScoreBadge, SeverityStatusBadge } from '@kbn/cloud-security-posture';
import { getVendorName } from '../../common/utils/get_vendor_name';
import { CloudSecurityDataTable } from '../../components/cloud_security_data_table';
import { useLatestVulnerabilitiesTable } from './hooks/use_latest_vulnerabilities_table';
@ -19,7 +20,6 @@ import { LATEST_VULNERABILITIES_TABLE } from './test_subjects';
import { getDefaultQuery, defaultColumns } from './constants';
import { VulnerabilityFindingFlyout } from './vulnerabilities_finding_flyout/vulnerability_finding_flyout';
import { ErrorCallout } from '../configurations/layout/error_callout';
import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges';
import { createDetectionRuleFromVulnerabilityFinding } from './utils/create_detection_rule_from_vulnerability';
import { vulnerabilitiesTableFieldLabels } from './vulnerabilities_table_field_labels';

View file

@ -28,13 +28,13 @@ import { euiThemeVars } from '@kbn/ui-theme';
import { css } from '@emotion/react';
import { HttpSetup } from '@kbn/core-http-browser';
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import { SeverityStatusBadge } from '@kbn/cloud-security-posture';
import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding';
import { TakeAction } from '../../../components/take_action';
import { truthy } from '../../../../common/utils/helpers';
import { CspInlineDescriptionList } from '../../../components/csp_inline_description_list';
import { VulnerabilityOverviewTab } from './vulnerability_overview_tab';
import { VulnerabilityJsonTab } from './vulnerability_json_tab';
import { SeverityStatusBadge } from '../../../components/vulnerability_badges';
import {
FINDINGS_VULNERABILITY_FLYOUT_DESCRIPTION_LIST,
TAB_ID_VULNERABILITY_FLYOUT,

View file

@ -28,10 +28,10 @@ import {
VULNERABILITIES_FLYOUT_VISITS,
uiMetricService,
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import { CVSScoreBadge } from '@kbn/cloud-security-posture';
import { getVendorName } from '../../../common/utils/get_vendor_name';
import { CspFlyoutMarkdown } from '../../configurations/findings_flyout/findings_flyout';
import { NvdLogo } from '../../../assets/icons/nvd_logo_svg';
import { CVSScoreBadge } from '../../../components/vulnerability_badges';
import { CVSScoreProps, Vendor } from '../types';
import { getVectorScoreList } from '../utils/get_vector_score_list';
import {

View file

@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n';
import type { NavFilter } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings';
import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings';
import type { VulnSeverity } from '@kbn/cloud-security-posture-common';
import { CVSScoreBadge, SeverityStatusBadge } from '@kbn/cloud-security-posture';
import {
PatchableVulnerabilityStat,
VulnerabilityStat,
@ -26,7 +27,6 @@ import {
} from '../../../common/types_old';
import { DASHBOARD_TABLE_TYPES } from './vulnerability_table_panel.config';
import { VulnerabilityTablePanel } from './vulnerability_table_panel';
import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges';
import { useVulnerabilityDashboardApi } from '../../common/api/use_vulnerability_dashboard_api';
import { VULNERABILITY_GROUPING_OPTIONS, VULNERABILITY_FIELDS } from '../../common/constants';

View file

@ -5,19 +5,134 @@
* 2.0.
*/
import React, { memo } from 'react';
import { EuiSpacer } from '@elastic/eui';
import React, { memo, useMemo, useState } from 'react';
import type { EuiButtonGroupOptionProps } from '@elastic/eui';
import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
import { useExpandableFlyoutState } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
// import type { FlyoutPanels } from '@kbn/expandable-flyout/src/store/state';
import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table';
import { VulnerabilitiesFindingsDetailsTable } from './vulnerabilities_findings_details_table';
/**
* Insights view displayed in the document details expandable flyout left section
*/
interface CspFlyoutPanelProps extends FlyoutPanelProps {
params: {
path: PanelPath;
hasMisconfigurationFindings: boolean;
hasVulnerabilitiesFindings: boolean;
};
}
// Type guard to check if the panel is a CspFlyoutPanelProps
function isCspFlyoutPanelProps(
panelLeft: FlyoutPanelProps | undefined
): panelLeft is CspFlyoutPanelProps {
return (
!!panelLeft?.params?.hasMisconfigurationFindings ||
!!panelLeft?.params?.hasVulnerabilitiesFindings
);
}
export const InsightsTabCsp = memo(
({ name, fieldName }: { name: string; fieldName: 'host.name' | 'user.name' }) => {
const panels = useExpandableFlyoutState();
let hasMisconfigurationFindings = false;
let hasVulnerabilitiesFindings = false;
let subTab: string | undefined;
// Check if panels.left is of type CspFlyoutPanelProps and extract values
if (isCspFlyoutPanelProps(panels.left)) {
hasMisconfigurationFindings = panels.left.params.hasMisconfigurationFindings;
hasVulnerabilitiesFindings = panels.left.params.hasVulnerabilitiesFindings;
subTab = panels.left.params.path?.subTab;
}
const getDefaultTab = () => {
if (subTab) {
return subTab;
}
return hasMisconfigurationFindings
? CspInsightLeftPanelSubTab.MISCONFIGURATIONS
: hasVulnerabilitiesFindings
? CspInsightLeftPanelSubTab.VULNERABILITIES
: '';
};
const [activeInsightsId, setActiveInsightsId] = useState(getDefaultTab());
const insightsButtons: EuiButtonGroupOptionProps[] = useMemo(() => {
const buttons: EuiButtonGroupOptionProps[] = [];
if (panels.left?.params?.hasMisconfigurationFindings) {
buttons.push({
id: CspInsightLeftPanelSubTab.MISCONFIGURATIONS,
label: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.misconfigurationButtonLabel"
defaultMessage="Misconfiguration"
/>
),
'data-test-subj': 'misconfigurationTabDataTestId',
});
}
if (panels.left?.params?.hasVulnerabilitiesFindings) {
buttons.push({
id: CspInsightLeftPanelSubTab.VULNERABILITIES,
label: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.vulnerabilitiesButtonLabel"
defaultMessage="Vulnerabilities"
/>
),
'data-test-subj': 'vulnerabilitiesTabDataTestId',
});
}
return buttons;
}, [
panels.left?.params?.hasMisconfigurationFindings,
panels.left?.params?.hasVulnerabilitiesFindings,
]);
const onTabChange = (id: string) => {
setActiveInsightsId(id);
};
if (insightsButtons.length === 0) {
return null;
}
return (
<>
<EuiButtonGroup
color="primary"
legend={i18n.translate(
'xpack.securitySolution.flyout.left.insights.optionsButtonGroups',
{
defaultMessage: 'Insights options',
}
)}
options={insightsButtons}
idSelected={activeInsightsId}
onChange={onTabChange}
buttonSize="compressed"
isFullWidth
data-test-subj={'insightButtonGroupsTestId'}
/>
<EuiSpacer size="xl" />
<MisconfigurationFindingsDetailsTable fieldName={fieldName} queryName={name} />
{activeInsightsId === CspInsightLeftPanelSubTab.MISCONFIGURATIONS ? (
<MisconfigurationFindingsDetailsTable fieldName={fieldName} queryName={name} />
) : (
<VulnerabilitiesFindingsDetailsTable queryName={name} />
)}
</>
);
}

View file

@ -18,6 +18,7 @@ import { useNavigateFindings } from '@kbn/cloud-security-posture/src/hooks/use_n
import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-common/schema/rules/latest';
import { CspEvaluationBadge } from '@kbn/cloud-security-posture';
import {
ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS,
NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT,
NAV_TO_FINDINGS_BY_RULE_NAME_FRPOM_ENTITY_FLYOUT,
uiMetricService,
@ -57,6 +58,7 @@ const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: numb
*/
export const MisconfigurationFindingsDetailsTable = memo(
({ fieldName, queryName }: { fieldName: 'host.name' | 'user.name'; queryName: string }) => {
uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS);
const { data } = useMisconfigurationFindings({
query: buildEntityFlyoutPreviewQuery(fieldName, queryName),
sort: [],

View file

@ -0,0 +1,224 @@
/*
* 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, { memo, useState } from 'react';
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import { EuiSpacer, EuiIcon, EuiPanel, EuiLink, EuiText, EuiBasicTable } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { VulnSeverity } from '@kbn/cloud-security-posture-common';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings';
import { useVulnerabilitiesFindings } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings';
import type {
CspVulnerabilityFinding,
Vulnerability,
} from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding';
import {
getVulnerabilityStats,
CVSScoreBadge,
SeverityStatusBadge,
} from '@kbn/cloud-security-posture';
import {
ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS,
NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT,
uiMetricService,
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import { METRIC_TYPE } from '@kbn/analytics';
type VulnerabilitiesFindingDetailFields = Pick<
CspVulnerabilityFinding,
'vulnerability' | 'resource'
>;
interface VulnerabilitiesPackage extends Vulnerability {
package: {
name: string;
};
}
export const VulnerabilitiesFindingsDetailsTable = memo(({ queryName }: { queryName: string }) => {
uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS);
const { data } = useVulnerabilitiesFindings({
query: buildEntityFlyoutPreviewQuery('host.name', queryName),
sort: [],
enabled: true,
pageSize: 1,
});
const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const findingsPagination = (findings: VulnerabilitiesFindingDetailFields[]) => {
let pageOfItems;
if (!pageIndex && !pageSize) {
pageOfItems = findings;
} else {
const startIndex = pageIndex * pageSize;
pageOfItems = findings?.slice(startIndex, Math.min(startIndex + pageSize, findings?.length));
}
return {
pageOfItems,
totalItemCount: findings?.length,
};
};
const { pageOfItems, totalItemCount } = findingsPagination(data?.rows || []);
const pagination = {
pageIndex,
pageSize,
totalItemCount,
pageSizeOptions: [10, 25, 100],
};
const onTableChange = ({ page }: Criteria<VulnerabilitiesFindingDetailFields>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
};
const navToVulnerabilities = useNavigateVulnerabilities();
const navToVulnerabilitiesByName = (name: string, queryField: 'host.name' | 'user.name') => {
navToVulnerabilities({ [queryField]: name });
};
const navToVulnerabilityByVulnerabilityAndResourceId = (
vulnerabilityId: string,
resourceId: string
) => {
navToVulnerabilities({
'vulnerability.id': vulnerabilityId,
'resource.id': resourceId,
});
};
const columns: Array<EuiBasicTableColumn<VulnerabilitiesFindingDetailFields>> = [
{
field: 'vulnerability',
name: '',
width: '5%',
render: (
vulnerability: VulnerabilitiesPackage,
finding: VulnerabilitiesFindingDetailFields
) => (
<EuiLink
onClick={() => {
navToVulnerabilityByVulnerabilityAndResourceId(
vulnerability?.id,
finding?.resource?.id || ''
);
}}
>
<EuiIcon type={'popout'} />
</EuiLink>
),
},
{
field: 'vulnerability',
render: (vulnerability: Vulnerability) => <EuiText size="s">{vulnerability?.id}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.vulnerability.table.resultColumnName',
{ defaultMessage: 'Vulnerability' }
),
width: '20%',
},
{
field: 'vulnerability',
render: (vulnerability: Vulnerability) => (
<EuiText size="s">
<CVSScoreBadge
version={vulnerability?.score?.version}
score={vulnerability?.score?.base}
/>
</EuiText>
),
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
{ defaultMessage: 'CVSS' }
),
width: '12.5%',
},
{
field: 'vulnerability',
render: (vulnerability: Vulnerability) => (
<>
<EuiText size="s">
<SeverityStatusBadge
severity={vulnerability?.severity?.toUpperCase() as VulnSeverity}
/>
</EuiText>
</>
),
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
{ defaultMessage: 'Severity' }
),
width: '12.5%',
},
{
field: 'vulnerability',
render: (vulnerability: VulnerabilitiesPackage) => (
<EuiText size="s">{vulnerability?.package?.name}</EuiText>
),
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
{ defaultMessage: 'Package' }
),
width: '50%',
},
];
return (
<>
<EuiPanel hasShadow={false}>
<EuiLink
onClick={() => {
uiMetricService.trackUiMetric(
METRIC_TYPE.CLICK,
NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT
);
navToVulnerabilitiesByName(queryName, 'host.name');
}}
>
{i18n.translate('xpack.securitySolution.flyout.left.insights.vulnerability.tableTitle', {
defaultMessage: 'Vulnerability ',
})}
<EuiIcon type={'popout'} />
</EuiLink>
<EuiSpacer size="xl" />
<DistributionBar
stats={getVulnerabilityStats({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
})}
/>
<EuiSpacer size="l" />
<EuiBasicTable
items={pageOfItems || []}
rowHeader="result"
columns={columns}
pagination={pagination}
onChange={onTableChange}
data-test-subj={'securitySolutionFlyoutVulnerabilitiesFindingsTable'}
/>
</EuiPanel>
</>
);
});
VulnerabilitiesFindingsDetailsTable.displayName = 'VulnerabilitiesFindingsDetailsTable';

View file

@ -10,7 +10,10 @@ import { EuiAccordion, EuiHorizontalRule, EuiSpacer, EuiTitle, useEuiTheme } fro
import React from 'react';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview';
import { VulnerabilitiesPreview } from './vulnerabilities/vulnerabilities_preview';
@ -24,10 +27,37 @@ export const EntityInsight = <T,>({
isPreviewMode?: boolean;
}) => {
const { euiTheme } = useEuiTheme();
const getSetupStatus = useCspSetupStatusApi();
const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings;
const hasVulnerabilitiesFindings = getSetupStatus.data?.hasVulnerabilitiesFindings;
const insightContent: React.ReactElement[] = [];
const { data: dataMisconfiguration } = useMisconfigurationPreview({
query: buildEntityFlyoutPreviewQuery(fieldName, name),
sort: [],
enabled: true,
pageSize: 1,
});
const passedFindings = dataMisconfiguration?.count.passed || 0;
const failedFindings = dataMisconfiguration?.count.failed || 0;
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const { data } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery(fieldName, name),
sort: [],
enabled: true,
pageSize: 1,
});
const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
const hasVulnerabilitiesFindings = hasVulnerabilitiesData({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
});
const isVulnerabilitiesFindingForHost = hasVulnerabilitiesFindings && fieldName === 'host.name';
if (hasMisconfigurationFindings)
@ -37,16 +67,17 @@ export const EntityInsight = <T,>({
<EuiSpacer size="m" />
</>
);
if (isVulnerabilitiesFindingForHost)
if (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings)
insightContent.push(
<>
<VulnerabilitiesPreview hostName={name} />
<VulnerabilitiesPreview name={name} isPreviewMode={isPreviewMode} />
<EuiSpacer size="m" />
</>
);
return (
<>
{(hasMisconfigurationFindings || isVulnerabilitiesFindingForHost) && (
{(hasMisconfigurationFindings ||
(isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings)) && (
<>
<EuiAccordion
initialIsOpen={true}

View file

@ -5,24 +5,44 @@
* 2.0.
*/
import { TestProviders } from '../../../common/mock';
import { render } from '@testing-library/react';
import React from 'react';
import { render } from '@testing-library/react';
import { MisconfigurationsPreview } from './misconfiguration_preview';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { TestProviders } from '../../../common/mock/test_providers';
const mockProps: { name: string; fieldName: 'host.name' | 'user.name' } = {
name: 'testContextID',
fieldName: 'host.name',
};
// Mock hooks
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
jest.mock('../../../entity_analytics/api/hooks/use_risk_score');
jest.mock('@kbn/expandable-flyout');
describe('MisconfigurationsPreview', () => {
it('renders', () => {
const { queryByTestId } = render(<MisconfigurationsPreview {...mockProps} />, {
wrapper: TestProviders,
const mockOpenLeftPanel = jest.fn();
beforeEach(() => {
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel });
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
});
(useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] });
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
data: { count: { passed: 1, failed: 1 } },
});
});
it('renders', () => {
const { getByTestId } = render(
<TestProviders>
<MisconfigurationsPreview name="host1" fieldName="host.name" />
</TestProviders>
);
expect(
queryByTestId('securitySolutionFlyoutInsightsMisconfigurationsContent')
getByTestId('securitySolutionFlyoutInsightsMisconfigurationsTitleLink')
).toBeInTheDocument();
expect(queryByTestId('noFindingsDataTestSubj')).toBeInTheDocument();
});
});

View file

@ -17,6 +17,12 @@ import { i18n } from '@kbn/i18n';
import { ExpandablePanel } from '@kbn/security-solution-common';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
import {
CspInsightLeftPanelSubTab,
EntityDetailsLeftPanelTab,
} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { UserDetailsPanelKey } from '../../../flyout/entity_details/user_details_left';
import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left';
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
@ -55,34 +61,6 @@ const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: numb
];
};
const MisconfigurationEmptyState = ({ euiTheme }: { euiTheme: EuiThemeComputed<{}> }) => {
return (
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiTitle size="m">
<h1>{'-'}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText
size="m"
css={css`
font-weight: ${euiTheme.font.weight.semiBold};
`}
data-test-subj="noFindingsDataTestSubj"
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.misconfigurations.noFindingsDescription"
defaultMessage="No Findings"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
};
const MisconfigurationPreviewScore = ({
passedFindings,
failedFindings,
@ -136,6 +114,7 @@ export const MisconfigurationsPreview = ({
sort: [],
enabled: true,
pageSize: 1,
ignore_unavailable: true,
});
const isUsingHostName = fieldName === 'host.name';
const passedFindings = data?.count.passed || 0;
@ -144,6 +123,29 @@ export const MisconfigurationsPreview = ({
const { euiTheme } = useEuiTheme();
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery('host.name', name),
sort: [],
enabled: true,
pageSize: 1,
});
const {
CRITICAL = 0,
HIGH = 0,
MEDIUM = 0,
LOW = 0,
NONE = 0,
} = vulnerabilitiesData?.count || {};
const hasVulnerabilitiesFindings = hasVulnerabilitiesData({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
});
const buildFilterQuery = useMemo(
() => (isUsingHostName ? buildHostNamesFilter([name]) : buildUserNamesFilter([name])),
[isUsingHostName, name]
@ -155,12 +157,17 @@ export const MisconfigurationsPreview = ({
onlyLatest: false,
pagination: FIRST_RECORD_PAGINATION,
});
const { data: hostRisk } = riskScoreState;
const riskData = hostRisk?.[0];
const isRiskScoreExist = isUsingHostName
? !!(riskData as HostRiskScore)?.host.risk
: !!(riskData as UserRiskScore)?.user.risk;
const { openLeftPanel } = useExpandableFlyoutApi();
const goToEntityInsightTab = useCallback(() => {
openLeftPanel({
id: isUsingHostName ? HostDetailsPanelKey : UserDetailsPanelKey,
@ -169,16 +176,27 @@ export const MisconfigurationsPreview = ({
name,
isRiskScoreExist,
hasMisconfigurationFindings,
path: { tab: 'csp_insights' },
hasVulnerabilitiesFindings,
path: {
tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS,
},
}
: {
user: { name },
isRiskScoreExist,
hasMisconfigurationFindings,
path: { tab: 'csp_insights' },
path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS },
},
});
}, [hasMisconfigurationFindings, isRiskScoreExist, isUsingHostName, name, openLeftPanel]);
}, [
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
isRiskScoreExist,
isUsingHostName,
name,
openLeftPanel,
]);
const link = useMemo(
() =>
!isPreviewMode
@ -216,15 +234,12 @@ export const MisconfigurationsPreview = ({
data-test-subj={'securitySolutionFlyoutInsightsMisconfigurations'}
>
<EuiFlexGroup gutterSize="none">
{hasMisconfigurationFindings ? (
<MisconfigurationPreviewScore
passedFindings={passedFindings}
failedFindings={failedFindings}
euiTheme={euiTheme}
/>
) : (
<MisconfigurationEmptyState euiTheme={euiTheme} />
)}
<MisconfigurationPreviewScore
passedFindings={passedFindings}
failedFindings={failedFindings}
euiTheme={euiTheme}
/>
<EuiFlexItem grow={2}>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem />

View file

@ -5,23 +5,44 @@
* 2.0.
*/
import { TestProviders } from '../../../common/mock';
import { render } from '@testing-library/react';
import React from 'react';
import { render } from '@testing-library/react';
import { VulnerabilitiesPreview } from './vulnerabilities_preview';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { TestProviders } from '../../../common/mock/test_providers';
const mockProps: { hostName: string } = {
hostName: 'testContextID',
};
// Mock hooks
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
jest.mock('../../../entity_analytics/api/hooks/use_risk_score');
jest.mock('@kbn/expandable-flyout');
describe('VulnerabilitiesPreview', () => {
it('renders', () => {
const { queryByTestId } = render(<VulnerabilitiesPreview {...mockProps} />, {
wrapper: TestProviders,
const mockOpenLeftPanel = jest.fn();
beforeEach(() => {
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel });
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
});
(useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] });
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
data: { count: { passed: 1, failed: 1 } },
});
});
it('renders', () => {
const { getByTestId } = render(
<TestProviders>
<VulnerabilitiesPreview name="host1" />
</TestProviders>
);
expect(
queryByTestId('securitySolutionFlyoutInsightsVulnerabilitiesContent')
getByTestId('securitySolutionFlyoutInsightsVulnerabilitiesTitleLink')
).toBeInTheDocument();
expect(queryByTestId('noVulnerabilitiesDataTestSubj')).toBeInTheDocument();
});
});

View file

@ -5,125 +5,30 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { css } from '@emotion/react';
import type { EuiThemeComputed } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { i18n } from '@kbn/i18n';
import { ExpandablePanel } from '@kbn/security-solution-common';
import {
buildEntityFlyoutPreviewQuery,
VULNERABILITIES_SEVERITY,
getAbbreviatedNumber,
} from '@kbn/cloud-security-posture-common';
import { getSeverityStatusColor, getSeverityText } from '@kbn/cloud-security-posture';
import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left';
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
import { buildHostNamesFilter } from '../../../../common/search_strategy';
interface VulnerabilitiesDistributionBarProps {
key: string;
count: number;
color: string;
}
const getVulnerabilityStats = (
critical: number,
high: number,
medium: number,
low: number,
none: number
): VulnerabilitiesDistributionBarProps[] => {
const vulnerabilityStats: VulnerabilitiesDistributionBarProps[] = [];
if (critical === 0 && high === 0 && medium === 0 && low === 0 && none === 0)
return vulnerabilityStats;
if (none > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.UNKNOWN),
}
),
count: none,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.UNKNOWN),
});
if (low > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.lowVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.LOW),
}
),
count: low,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.LOW),
});
if (medium > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.mediumVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.MEDIUM),
}
),
count: medium,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.MEDIUM),
});
if (high > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.highVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.HIGH),
}
),
count: high,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.HIGH),
});
if (critical > 0)
vulnerabilityStats.push({
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.CriticalVulnerabilitiesText',
{
defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.CRITICAL),
}
),
count: critical,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.CRITICAL),
});
return vulnerabilityStats;
};
const VulnerabilitiesEmptyState = ({ euiTheme }: { euiTheme: EuiThemeComputed<{}> }) => {
return (
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiTitle size="m">
<h1>{'-'}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText
size="m"
css={css`
font-weight: ${euiTheme.font.weight.semiBold};
`}
data-test-subj="noVulnerabilitiesDataTestSubj"
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.vulnerabilities.noVulnerabilitiesDescription"
defaultMessage="No vulnerabilities"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
const FIRST_RECORD_PAGINATION = {
cursorStart: 0,
querySize: 1,
};
const VulnerabilitiesCount = ({
@ -159,9 +64,15 @@ const VulnerabilitiesCount = ({
);
};
export const VulnerabilitiesPreview = ({ hostName }: { hostName: string }) => {
export const VulnerabilitiesPreview = ({
name,
isPreviewMode,
}: {
name: string;
isPreviewMode?: boolean;
}) => {
const { data } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
query: buildEntityFlyoutPreviewQuery('host.name', name),
sort: [],
enabled: true,
pageSize: 1,
@ -170,11 +81,77 @@ export const VulnerabilitiesPreview = ({ hostName }: { hostName: string }) => {
const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
const totalVulnerabilities = CRITICAL + HIGH + MEDIUM + LOW + NONE;
const hasVulnerabilitiesFindings = hasVulnerabilitiesData({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
});
const { euiTheme } = useEuiTheme();
const hasVulnerabilities = totalVulnerabilities > 0;
const { data: dataMisconfiguration } = useMisconfigurationPreview({
query: buildEntityFlyoutPreviewQuery('host.name', name),
sort: [],
enabled: true,
pageSize: 1,
});
const passedFindings = dataMisconfiguration?.count.passed || 0;
const failedFindings = dataMisconfiguration?.count.failed || 0;
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const buildFilterQuery = useMemo(() => buildHostNamesFilter([name]), [name]);
const riskScoreState = useRiskScore({
riskEntity: RiskScoreEntity.host,
filterQuery: buildFilterQuery,
onlyLatest: false,
pagination: FIRST_RECORD_PAGINATION,
});
const { data: hostRisk } = riskScoreState;
const riskData = hostRisk?.[0];
const isRiskScoreExist = riskData?.host.risk;
const { openLeftPanel } = useExpandableFlyoutApi();
const goToEntityInsightTab = useCallback(() => {
openLeftPanel({
id: HostDetailsPanelKey,
params: {
name,
isRiskScoreExist,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, subTab: 'vulnerabilitiesTabId' },
},
});
}, [
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
isRiskScoreExist,
name,
openLeftPanel,
]);
const link = useMemo(
() =>
!isPreviewMode
? {
callback: goToEntityInsightTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.misconfiguration.misconfigurationTooltip"
defaultMessage="Show all misconfiguration findings"
/>
),
}
: undefined,
[isPreviewMode, goToEntityInsightTab]
);
return (
<ExpandablePanel
header={{
iconType: !isPreviewMode && hasVulnerabilitiesFindings ? 'arrowStart' : '',
title: (
<EuiText
size="xs"
@ -188,24 +165,29 @@ export const VulnerabilitiesPreview = ({ hostName }: { hostName: string }) => {
/>
</EuiText>
),
link,
}}
data-test-subj={'securitySolutionFlyoutInsightsVulnerabilities'}
>
<EuiFlexGroup gutterSize="none">
{hasVulnerabilities ? (
<VulnerabilitiesCount
vulnerabilitiesTotal={getAbbreviatedNumber(totalVulnerabilities)}
euiTheme={euiTheme}
/>
) : (
<VulnerabilitiesEmptyState euiTheme={euiTheme} />
)}
<VulnerabilitiesCount
vulnerabilitiesTotal={getAbbreviatedNumber(totalVulnerabilities)}
euiTheme={euiTheme}
/>
<EuiFlexItem grow={2}>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem />
<EuiFlexItem>
<EuiSpacer />
<DistributionBar stats={getVulnerabilityStats(CRITICAL, HIGH, MEDIUM, LOW, NONE)} />
<DistributionBar
stats={getVulnerabilityStats({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -7,11 +7,6 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS,
uiMetricService,
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import { METRIC_TYPE } from '@kbn/analytics';
import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { PREFIX } from '../../../flyout/shared/test_ids';
import type { RiskInputsTabProps } from './tabs/risk_inputs/risk_inputs_tab';
@ -40,8 +35,6 @@ export const getInsightsInputTab = ({
name: string;
fieldName: 'host.name' | 'user.name';
}) => {
uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS);
return {
id: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
'data-test-subj': INSIGHTS_TAB_TEST_ID,

View file

@ -12,6 +12,7 @@ import {
getInsightsInputTab,
} from '../../../entity_analytics/components/entity_details_flyout';
import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content';
import type { CspInsightLeftPanelSubTab } from '../shared/components/left_panel/left_panel_header';
import {
EntityDetailsLeftPanelTab,
LeftPanelHeader,
@ -23,8 +24,10 @@ export interface HostDetailsPanelProps extends Record<string, unknown> {
name: string;
scopeId: string;
hasMisconfigurationFindings?: boolean;
hasVulnerabilitiesFindings?: boolean;
path?: {
tab?: EntityDetailsLeftPanelTab;
subTab?: CspInsightLeftPanelSubTab;
};
}
export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps {
@ -39,6 +42,7 @@ export const HostDetailsPanel = ({
scopeId,
path,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
}: HostDetailsPanelProps) => {
const [selectedTabId, setSelectedTabId] = useState(
path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS
@ -53,11 +57,12 @@ export const HostDetailsPanel = ({
: [];
// Determine if the Insights tab should be included
const insightsTab = hasMisconfigurationFindings
? [getInsightsInputTab({ name, fieldName: 'host.name' })]
: [];
const insightsTab =
hasMisconfigurationFindings || hasVulnerabilitiesFindings
? [getInsightsInputTab({ name, fieldName: 'host.name' })]
: [];
return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}];
}, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings]);
}, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings, hasVulnerabilitiesFindings]);
return (
<>

View file

@ -8,7 +8,7 @@
import React from 'react';
import { EuiHorizontalRule } from '@elastic/eui';
import { FlyoutBody } from '@kbn/security-solution-common';
import { EntityInsight } from '../../../cloud_security_posture/components';
import { EntityInsight } from '../../../cloud_security_posture/components/entity_insight';
import { AssetCriticalityAccordion } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector';
import { FlyoutRiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary';
import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score';

View file

@ -12,6 +12,8 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { FlyoutLoading, FlyoutNavigation } from '@kbn/security-solution-common';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { sum } from 'lodash';
import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id';
import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab';
import type { Refetch } from '../../../common/types';
@ -107,6 +109,15 @@ export const HostPanel = ({
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
sort: [],
enabled: true,
pageSize: 1,
});
const hasVulnerabilitiesFindings = sum(Object.values(vulnerabilitiesData?.count || {})) > 0;
useQueryInspector({
deleteQuery,
inspect: inspectRiskScore,
@ -130,10 +141,19 @@ export const HostPanel = ({
isRiskScoreExist,
path: tab ? { tab } : undefined,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
},
});
},
[telemetry, openLeftPanel, hostName, scopeId, isRiskScoreExist, hasMisconfigurationFindings]
[
telemetry,
openLeftPanel,
hostName,
scopeId,
isRiskScoreExist,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
]
);
const openDefaultPanel = useCallback(
@ -173,7 +193,8 @@ export const HostPanel = ({
<>
<FlyoutNavigation
flyoutIsExpandable={
!isPreviewMode && (isRiskScoreExist || hasMisconfigurationFindings)
!isPreviewMode &&
(isRiskScoreExist || hasMisconfigurationFindings || hasVulnerabilitiesFindings)
}
expandDetails={openDefaultPanel}
/>

View file

@ -25,6 +25,11 @@ export enum EntityDetailsLeftPanelTab {
CSP_INSIGHTS = 'csp_insights',
}
export enum CspInsightLeftPanelSubTab {
MISCONFIGURATIONS = 'misconfigurationTabId',
VULNERABILITIES = 'vulnerabilitiesTabId',
}
export interface PanelHeaderProps {
/**
* Id of the tab selected in the parent component to display its content

View file

@ -23,7 +23,7 @@ import { ObservedEntity } from '../shared/components/observed_entity';
import type { ObservedEntityData } from '../shared/components/observed_entity/types';
import { useObservedUserItems } from './hooks/use_observed_user_items';
import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
import { EntityInsight } from '../../../cloud_security_posture/components';
import { EntityInsight } from '../../../cloud_security_posture/components/entity_insight';
interface UserPanelContentProps {
userName: string;

View file

@ -30,7 +30,7 @@ import { UserPanelContent } from './content';
import { UserPanelHeader } from './header';
import { UserDetailsPanelKey } from '../user_details_left';
import { useObservedUser } from './hooks/use_observed_user';
import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
import { UserPreviewPanelFooter } from '../user_preview/footer';
export interface UserPanelProps extends Record<string, unknown> {
@ -83,6 +83,7 @@ export const UserPanel = ({
const { data: userRisk } = riskScoreState;
const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined;
const isRiskScoreExist = !!userRiskData?.user.risk;
const refetchRiskInputsTab = useRefetchQueryById(RISK_INPUTS_TAB_QUERY_ID);
const refetchRiskScore = useCallback(() => {
@ -149,8 +150,15 @@ export const UserPanel = ({
hasMisconfigurationFindings,
]
);
const openPanelFirstTab = useCallback(() => openPanelTab(), [openPanelTab]);
const openPanelFirstTab = useCallback(
() =>
openPanelTab(
isRiskScoreExist
? EntityDetailsLeftPanelTab.RISK_INPUTS
: EntityDetailsLeftPanelTab.CSP_INSIGHTS
),
[isRiskScoreExist, openPanelTab]
);
const hasUserDetailsData =
!!userRiskData?.user.risk ||

View file

@ -18,10 +18,12 @@ import { ALERTS_URL } from '../../../../urls/navigation';
import { visit } from '../../../../tasks/navigation';
const CSP_INSIGHT_VULNERABILITIES_TITLE = getDataTestSubjectSelector(
'securitySolutionFlyoutInsightsVulnerabilitiesTitleText'
'securitySolutionFlyoutInsightsVulnerabilitiesTitleLink'
);
const NO_VULNERABILITIES_TEXT = getDataTestSubjectSelector('noVulnerabilitiesDataTestSubj');
const CSP_INSIGHT_VULNERABILITIES_TABLE = getDataTestSubjectSelector(
'securitySolutionFlyoutVulnerabilitiesFindingsTable'
);
const timestamp = Date.now();
@ -154,6 +156,28 @@ describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless']
});
});
context(
'Host name - Has Vulnerabilities findings but with different host name than the alerts',
() => {
beforeEach(() => {
createMockVulnerability(false);
cy.reload();
expandFirstAlertHostFlyout();
});
afterEach(() => {
deleteDataStream();
});
it('should display Vulnerabilities preview under Insights Entities when it has Vulnerabilities Findings', () => {
expandFirstAlertHostFlyout();
cy.log('check if Vulnerabilities preview title is not shown');
cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).should('not.exist');
});
}
);
context('Host name - Has Vulnerabilities findings', () => {
beforeEach(() => {
createMockVulnerability(true);
@ -169,27 +193,10 @@ describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless']
cy.log('check if Vulnerabilities preview title shown');
cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).should('be.visible');
});
it('should display insight tabs and findings table upon clicking on misconfiguration accordion', () => {
cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).click();
cy.get(CSP_INSIGHT_VULNERABILITIES_TABLE).should('be.visible');
});
});
context(
'Host name - Has Vulnerabilities findings but host name is not the same as alert host name',
() => {
beforeEach(() => {
createMockVulnerability(false);
cy.reload();
expandFirstAlertHostFlyout();
});
afterEach(() => {
deleteDataStream();
});
it('should display Vulnerabilities preview under Insights Entities when it has Vulnerabilities Findings but it should show no vulnerabilities title', () => {
cy.log('check if Vulnerabilities preview title shown');
cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).should('be.visible');
cy.log('check if no vulnerabilities text is shown');
cy.get(NO_VULNERABILITIES_TEXT).should('be.visible');
});
}
);
});