mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
fbf3f8b8b2
commit
59f2f85b8a
36 changed files with 1007 additions and 330 deletions
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -5,7 +5,8 @@
|
|||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
"react",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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';
|
|
@ -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}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue