[Cloud Security] 3P integrations callouts (#194362)

This commit is contained in:
Jordan 2024-10-04 20:27:56 +03:00 committed by GitHub
parent 5be5e36cf4
commit d3f3d34519
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 277 additions and 100 deletions

View file

@ -18,6 +18,7 @@ export type {
CspSetupStatus,
} from './types/status';
export type { CspFinding, CspFindingResult } from './types/findings';
export type { CspVulnerabilityFinding } from './schema/vulnerabilities/csp_vulnerability_finding';
export type { BenchmarksCisId } from './types/benchmark';
export type { VulnSeverity } from './types/vulnerabilities';
export * from './constants';

View file

@ -6,7 +6,7 @@
*/
// TODO: this needs to be defined in a versioned schema
import type { EcsEvent } from '@elastic/ecs';
import type { EcsDataStream, EcsEvent, EcsObserver } from '@elastic/ecs';
import type { VulnSeverity } from '../../types/vulnerabilities';
export interface CspVulnerabilityFinding {
@ -75,7 +75,8 @@ export interface CspVulnerabilityFinding {
name?: string;
fixed_version?: string;
};
data_stream: { dataset: string };
data_stream: EcsDataStream;
observer: EcsObserver;
}
export interface Vulnerability {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { EcsDataStream, EcsEvent } from '@elastic/ecs';
import type { EcsDataStream, EcsEvent, EcsObserver } from '@elastic/ecs';
import type { CspBenchmarkRuleMetadata } from '../schema/rules/latest';
export interface CspFinding {
@ -19,6 +19,7 @@ export interface CspFinding {
host: CspFindingHost;
event: EcsEvent;
data_stream: EcsDataStream;
observer: EcsObserver;
agent: CspFindingAgent;
ecs: {
version: string;

View file

@ -38,6 +38,9 @@ export const LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY =
'cloudPosture:complianceDashboard:benchmarkSort';
export const LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY = 'cloudPosture:findings:lastSelectedTab';
export const LOCAL_STORAGE_3P_INTEGRATIONS_CALLOUT_KEY =
'cloudPosture:findings:3pIntegrationsCallout';
export const LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY = 'cspLatestVulnerabilitiesGrouping';
export const LOCAL_STORAGE_FINDINGS_GROUPING_KEY = 'cspLatestFindingsGrouping';
@ -230,6 +233,7 @@ export const FINDINGS_GROUPING_OPTIONS = {
CLOUD_ACCOUNT_NAME: 'cloud.account.name',
ORCHESTRATOR_CLUSTER_NAME: 'orchestrator.cluster.name',
};
export const VULNERABILITY_FIELDS = {
VULNERABILITY_ID: 'vulnerability.id',
SCORE_BASE: 'vulnerability.score.base',
@ -242,8 +246,9 @@ export const VULNERABILITY_FIELDS = {
CLOUD_ACCOUNT_NAME: 'cloud.account.name',
CLOUD_PROVIDER: 'cloud.provider',
DESCRIPTION: 'vulnerability.description',
SOURCE: 'data_stream.dataset',
VENDOR: 'observer.vendor',
} as const;
export const VULNERABILITY_GROUPING_OPTIONS = {
NONE: 'none',
RESOURCE_NAME: VULNERABILITY_FIELDS.RESOURCE_NAME,

View file

@ -1,42 +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 {
CSP_MISCONFIGURATIONS_DATASET,
CSP_VULN_DATASET,
getDatasetDisplayName,
WIZ_MISCONFIGURATIONS_DATASET,
WIZ_VULN_DATASET,
} from './get_dataset_display_name';
describe('getDatasetDisplayName', () => {
it('should return "Wiz" when dataset is from Wiz integration', () => {
const wizMisconfigsDatasetDisplayName = getDatasetDisplayName(WIZ_MISCONFIGURATIONS_DATASET);
expect(wizMisconfigsDatasetDisplayName).toBe('Wiz');
const wizVulnDatasetDisplayName = getDatasetDisplayName(WIZ_VULN_DATASET);
expect(wizVulnDatasetDisplayName).toBe('Wiz');
});
it('should return "Elastic CSP" when dataset is from Elastic CSP integration', () => {
const elasticCspMisconfigsDatasetDisplayName = getDatasetDisplayName(
CSP_MISCONFIGURATIONS_DATASET
);
expect(elasticCspMisconfigsDatasetDisplayName).toBe('Elastic CSP');
const elasticCspVulnDatasetDisplayName = getDatasetDisplayName(CSP_VULN_DATASET);
expect(elasticCspVulnDatasetDisplayName).toBe('Elastic CSP');
});
it('should return undefined when dataset is undefined', () => {
const result = getDatasetDisplayName(undefined);
expect(result).toBeUndefined();
});
it('should return undefined when dataset is an empty string', () => {
const result = getDatasetDisplayName('');
expect(result).toBeUndefined();
});
});

View file

@ -0,0 +1,71 @@
/*
* 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 { CspFinding, CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common';
import { getVendorName } from './get_vendor_name';
describe('getVendorName', () => {
it('should return the vendor from the finding if available', () => {
const finding = {
observer: { vendor: 'SomeVendor' },
data_stream: { dataset: 'some.dataset' },
} as CspFinding;
const result = getVendorName(finding);
expect(result).toBe('SomeVendor');
});
it('should return "Wiz" for Wiz misconfiguration dataset', () => {
const finding = {
observer: {},
data_stream: { dataset: 'wiz.cloud_configuration_finding' },
} as CspFinding;
const result = getVendorName(finding);
expect(result).toBe('Wiz');
});
it('should return "Wiz" for Wiz vulnerability dataset', () => {
const finding = {
observer: {},
data_stream: { dataset: 'wiz.vulnerability' },
} as CspVulnerabilityFinding;
const result = getVendorName(finding);
expect(result).toBe('Wiz');
});
it('should return "Elastic" for Elastic misconfiguration dataset', () => {
const finding = {
observer: {},
data_stream: { dataset: 'cloud_security_posture.findings' },
} as CspFinding;
const result = getVendorName(finding);
expect(result).toBe('Elastic');
});
it('should return "Elastic" for Elastic vulnerability dataset', () => {
const finding = {
observer: {},
data_stream: { dataset: 'cloud_security_posture.vulnerabilities' },
} as CspVulnerabilityFinding;
const result = getVendorName(finding);
expect(result).toBe('Elastic');
});
it('should return undefined if no vendor or known dataset is provided', () => {
const finding = {
observer: {},
data_stream: { dataset: 'unknown.dataset' },
} as CspFinding;
const result = getVendorName(finding);
expect(result).toBeUndefined();
});
});

View file

@ -5,15 +5,19 @@
* 2.0.
*/
type Dataset = 'wiz.cloud_configuration_finding' | 'cloud_security_posture.findings';
import { CspFinding, CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common';
import { isNativeCspFinding } from './is_native_csp_finding';
export const CSP_MISCONFIGURATIONS_DATASET = 'cloud_security_posture.findings';
export const CSP_VULN_DATASET = 'cloud_security_posture.vulnerabilities';
export const WIZ_MISCONFIGURATIONS_DATASET = 'wiz.cloud_configuration_finding';
export const WIZ_VULN_DATASET = 'wiz.vulnerability';
export const getDatasetDisplayName = (dataset?: Dataset | string) => {
export const getVendorName = (finding: CspFinding | CspVulnerabilityFinding) => {
if (finding.observer?.vendor) return finding.observer.vendor;
const dataset = finding.data_stream?.dataset;
if (dataset === WIZ_MISCONFIGURATIONS_DATASET || dataset === WIZ_VULN_DATASET) return 'Wiz';
if (dataset === CSP_MISCONFIGURATIONS_DATASET || dataset === CSP_VULN_DATASET)
return 'Elastic CSP';
if (isNativeCspFinding(finding)) return 'Elastic';
};

View file

@ -0,0 +1,56 @@
/*
* 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 { CSP_MISCONFIGURATIONS_DATASET, CSP_VULN_DATASET } from './get_vendor_name';
import { isNativeCspFinding } from './is_native_csp_finding';
import { CspFinding } from '@kbn/cloud-security-posture-common';
import { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding';
describe('isNativeCspFinding', () => {
it("should return true when finding's dataset matches CSP_MISCONFIGURATIONS_DATASET", () => {
const finding = {
data_stream: {
dataset: CSP_MISCONFIGURATIONS_DATASET,
},
} as CspFinding;
expect(isNativeCspFinding(finding)).toBe(true);
});
it("should return true when finding's dataset matches CSP_VULN_DATASET", () => {
const finding = {
data_stream: {
dataset: CSP_VULN_DATASET,
},
} as CspVulnerabilityFinding;
expect(isNativeCspFinding(finding)).toBe(true);
});
it('should return false when finding object is missing data_stream property', () => {
const finding = {} as CspFinding;
expect(isNativeCspFinding(finding)).toBe(false);
});
it('should return false when finding object has data_stream property but missing dataset property', () => {
const finding = {
data_stream: {},
} as CspFinding;
expect(isNativeCspFinding(finding)).toBe(false);
});
it('should return false when dataset property is null or undefined', () => {
const findingWithUndefinedDataset = {
data_stream: {
dataset: undefined,
},
} as CspFinding;
expect(isNativeCspFinding(findingWithUndefinedDataset)).toBe(false);
});
});

View file

@ -0,0 +1,14 @@
/*
* 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 { CspFinding } from '@kbn/cloud-security-posture-common';
import { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding';
import { CSP_MISCONFIGURATIONS_DATASET, CSP_VULN_DATASET } from './get_vendor_name';
export const isNativeCspFinding = (finding: CspFinding | CspVulnerabilityFinding) =>
finding.data_stream?.dataset === CSP_MISCONFIGURATIONS_DATASET ||
finding.data_stream?.dataset === CSP_VULN_DATASET;

View file

@ -118,6 +118,9 @@ export const mockFindingsHit: CspFinding = {
data_stream: {
dataset: 'cloud_security_posture.findings',
},
observer: {
vendor: 'Elastic',
},
};
export const mockWizFinding = {
@ -183,6 +186,9 @@ export const mockWizFinding = {
type: 'logs',
dataset: 'wiz.cloud_configuration_finding',
},
observer: {
vendor: 'Wiz',
},
event: {
agent_id_status: 'auth_metadata_missing',
ingested: '2024-07-15T10:49:45Z',

View file

@ -135,6 +135,9 @@ export const generateCspFinding = (
data_stream: {
dataset: 'cloud_security_posture.findings',
},
observer: {
vendor: 'Elastic',
},
};
};

View file

@ -37,10 +37,12 @@ import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { CspEvaluationBadge } from '@kbn/cloud-security-posture';
import type { CspFinding } from '@kbn/cloud-security-posture-common';
import { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding';
import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding';
import {
CSP_MISCONFIGURATIONS_DATASET,
getDatasetDisplayName,
} from '../../../common/utils/get_dataset_display_name';
getVendorName,
} from '../../../common/utils/get_vendor_name';
import { truthy } from '../../../../common/utils/helpers';
import { benchmarksNavigation } from '../../../common/navigation/constants';
import cisLogoIcon from '../../../assets/icons/cis_logo.svg';
@ -200,13 +202,13 @@ const FindingsTab = ({ tab, finding }: { finding: CspFinding; tab: FindingsTab }
}
};
const isNativeCspFinding = (finding: CspFinding) =>
finding.data_stream.dataset === CSP_MISCONFIGURATIONS_DATASET;
const MissingFieldsCallout = ({ finding }: { finding: CspFinding }) => {
export const MissingFieldsCallout = ({
finding,
}: {
finding: CspFinding | CspVulnerabilityFinding;
}) => {
const { euiTheme } = useEuiTheme();
const datasetDisplayName =
getDatasetDisplayName(finding.data_stream.dataset) || finding.data_stream.dataset;
const vendor = getVendorName(finding);
return (
<EuiCallOut
@ -220,9 +222,9 @@ const MissingFieldsCallout = ({ finding }: { finding: CspFinding }) => {
<span style={{ color: euiTheme.colors.text }}>
<FormattedMessage
id="xpack.csp.findings.findingsFlyout.calloutTitle"
defaultMessage="Some fields not provided by {datasource}"
defaultMessage="Some fields not provided by {vendor}"
values={{
datasource: datasetDisplayName || 'the data source',
vendor: vendor || 'the vendor',
}}
/>
</span>
@ -285,7 +287,7 @@ export const FindingsRuleFlyout = ({
</EuiFlyoutHeader>
<EuiFlyoutBody key={tab.id}>
{!isNativeCspFinding(finding) && ['overview', 'rule'].includes(tab.id) && (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: euiThemeVars.euiSize }}>
<MissingFieldsCallout finding={finding} />
</div>
)}

View file

@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { isEmpty } from 'lodash';
import type { CspFinding } from '@kbn/cloud-security-posture-common';
import { useDataView } from '@kbn/cloud-security-posture/src/hooks/use_data_view';
import { getDatasetDisplayName } from '../../../common/utils/get_dataset_display_name';
import { getVendorName } from '../../../common/utils/get_vendor_name';
import { truthy } from '../../../../common/utils/helpers';
import { CSP_MOMENT_FORMAT } from '../../../common/constants';
import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants';
@ -107,11 +107,10 @@ const getDetailsList = (
description: data.rule?.section ? data.rule?.section : EMPTY_VALUE,
},
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.sourceTitle', {
defaultMessage: 'Source',
title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.vendorTitle', {
defaultMessage: 'Vendor',
}),
description:
getDatasetDisplayName(data.data_stream?.dataset) || data.data_stream?.dataset || EMPTY_VALUE,
description: getVendorName(data) || EMPTY_VALUE,
},
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.dataViewTitle', {

View file

@ -43,13 +43,15 @@ export const getRuleList = (
defaultMessage: 'Alerts',
}),
description:
ruleState === 'unmuted' && rule?.benchmark?.name ? (
<RulesDetectionRuleCounter benchmarkRule={rule} />
) : (
ruleState === 'muted' ? (
<FormattedMessage
id="xpack.csp.findings.findingsFlyout.ruleTab.disabledRuleText"
defaultMessage="Disabled"
/>
) : rule?.benchmark?.name ? (
<RulesDetectionRuleCounter benchmarkRule={rule} />
) : (
EMPTY_VALUE
),
},
{

View file

@ -126,6 +126,6 @@ export const defaultColumns: CloudSecurityDefaultColumn[] = [
{ id: 'rule.benchmark.rule_number' },
{ id: 'rule.name' },
{ id: 'rule.section' },
{ id: 'data_stream.dataset' },
{ id: 'observer.vendor' },
{ id: '@timestamp' },
];

View file

@ -35,9 +35,9 @@ export const findingsTableFieldLabels: Record<string, string> = {
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel',
{ defaultMessage: 'Framework Section' }
),
'data_stream.dataset': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.sourceColumnLabel',
{ defaultMessage: 'Source' }
'observer.vendor': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.vendorColumnLabel',
{ defaultMessage: 'Vendor' }
),
'@timestamp': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel',

View file

@ -18,7 +18,7 @@ import {
uiMetricService,
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import { METRIC_TYPE } from '@kbn/analytics';
import { getDatasetDisplayName } from '../../../common/utils/get_dataset_display_name';
import { getVendorName } from '../../../common/utils/get_vendor_name';
import * as TEST_SUBJECTS from '../test_subjects';
import { FindingsDistributionBar } from '../layout/findings_distribution_bar';
import { ErrorCallout } from '../layout/error_callout';
@ -68,11 +68,13 @@ const customCellRenderer = (rows: DataTableRecord[]) => ({
return <CspEvaluationBadge type={finding?.result?.evaluation} />;
},
'data_stream.dataset': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
'observer.vendor': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
const finding = getCspFinding(rows[rowIndex].raw._source);
const source = getDatasetDisplayName(finding?.data_stream?.dataset);
if (!finding) return <>{''}</>;
return <>{source || finding?.data_stream?.dataset || ''}</>;
const vendor = getVendorName(finding);
return <>{vendor || ''}</>;
},
'@timestamp': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
const finding = getCspFinding(rows[rowIndex].raw._source);

View file

@ -6,15 +6,20 @@
*/
import React from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle, EuiCallOut, EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { Redirect, useHistory, useLocation, matchPath } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
import { findingsNavigation } from '@kbn/cloud-security-posture';
import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api';
import { i18n } from '@kbn/i18n';
import { useAdd3PIntegrationRoute } from '../../common/api/use_wiz_integration_route';
import { Configurations } from '../configurations';
import { cloudPosturePages } from '../../common/navigation/constants';
import { LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY } from '../../common/constants';
import {
LOCAL_STORAGE_3P_INTEGRATIONS_CALLOUT_KEY,
LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY,
} from '../../common/constants';
import { VULNERABILITIES_INDEX_NAME, FINDINGS_INDEX_NAME } from '../../../common/constants';
import { getStatusForIndexName } from '../../../common/utils/helpers';
import { Vulnerabilities } from '../vulnerabilities';
@ -59,7 +64,10 @@ const FindingsTabRedirecter = ({ lastTabSelected }: { lastTabSelected?: Findings
export const Findings = () => {
const history = useHistory();
const location = useLocation();
const wizAddIntegrationLink = useAdd3PIntegrationRoute('wiz');
const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage(
LOCAL_STORAGE_3P_INTEGRATIONS_CALLOUT_KEY
);
// restore the users most recent tab selection
const [lastTabSelected, setLastTabSelected] = useLocalStorage<FindingsTabKey>(
LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY
@ -101,6 +109,26 @@ export const Findings = () => {
</h1>
</EuiTitle>
<EuiSpacer />
{!userHasDismissedCallout && (
<>
<EuiCallOut
title={i18n.translate('xpack.csp.findings.3pIntegrationsCallout.title', {
defaultMessage:
"New! Ingest your cloud security product's data into Elastic for centralized analytics, hunting, investigations, visualizations, and more",
})}
iconType="cheer"
onDismiss={() => setUserHasDismissedCallout(true)}
>
<EuiButton href={wizAddIntegrationLink}>
<FormattedMessage
id="xpack.csp.findings.3pIntegrationsCallout.buttonTitle"
defaultMessage="Integrate Wiz"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer />
</>
)}
<EuiTabs size="l">
<EuiTab
key="configurations"

View file

@ -89,6 +89,9 @@ export const mockVulnerabilityHit: CspVulnerabilityFinding = {
data_stream: {
dataset: 'cloud_security_posture.vulnerabilities',
},
observer: {
vendor: 'Elastic',
},
};
export const mockWizVulnerabilityHit: CspVulnerabilityFinding = {
'@timestamp': '2023-03-30T10:27:35.013Z',
@ -159,6 +162,9 @@ export const mockWizVulnerabilityHit: CspVulnerabilityFinding = {
architecture: 'aarch64',
},
data_stream: {
dataset: 'cloud_security_posture.vulnerabilities',
dataset: 'wiz.vulnerability',
},
observer: {
vendor: 'Wiz',
},
};

View file

@ -49,5 +49,5 @@ export const defaultColumns: CloudSecurityDefaultColumn[] = [
{ id: VULNERABILITY_FIELDS.PACKAGE_NAME },
{ id: VULNERABILITY_FIELDS.PACKAGE_VERSION },
{ id: VULNERABILITY_FIELDS.PACKAGE_FIXED_VERSION },
{ id: VULNERABILITY_FIELDS.SOURCE },
{ id: VULNERABILITY_FIELDS.VENDOR },
];

View file

@ -12,7 +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 { getDatasetDisplayName } from '../../common/utils/get_dataset_display_name';
import { getVendorName } from '../../common/utils/get_vendor_name';
import { CloudSecurityDataTable } from '../../components/cloud_security_data_table';
import { useLatestVulnerabilitiesTable } from './hooks/use_latest_vulnerabilities_table';
import { LATEST_VULNERABILITIES_TABLE } from './test_subjects';
@ -89,9 +89,9 @@ const customCellRenderer = (rows: DataTableRecord[]) => ({
{({ finding }) => <SeverityStatusBadge severity={finding.vulnerability.severity} />}
</CspVulnerabilityFindingRenderer>
),
'data_stream.dataset': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
'observer.vendor': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
{({ finding }) => <>{getDatasetDisplayName(finding.data_stream.dataset) || '-'}</>}
{({ finding }) => <>{getVendorName(finding) || '-'}</>}
</CspVulnerabilityFindingRenderer>
),
});

View file

@ -8,7 +8,7 @@
import React from 'react';
import type { HttpSetup } from '@kbn/core/public';
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import { CSP_VULN_DATASET } from '../../../common/utils/get_dataset_display_name';
import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding';
import { DetectionRuleCounter } from '../../../components/detection_rule_counter';
import { createDetectionRuleFromVulnerabilityFinding } from '../utils/create_detection_rule_from_vulnerability';
@ -19,11 +19,12 @@ const CNVM_RULE_TAG_OS = 'OS: Linux';
const getTags = (vulnerabilityRecord: CspVulnerabilityFinding) => {
let tags = [vulnerabilityRecord.vulnerability.id];
const vendor = vulnerabilityRecord.observer?.vendor || vulnerabilityRecord?.data_stream?.dataset;
if (vulnerabilityRecord?.data_stream?.dataset === CSP_VULN_DATASET) {
if (isNativeCspFinding(vulnerabilityRecord)) {
tags = [CNVM_TAG, CNVM_RULE_TAG_DATA_SOURCE, CNVM_RULE_TAG_USE_CASE, CNVM_RULE_TAG_OS, ...tags];
} else {
tags.push(vulnerabilityRecord?.data_stream?.dataset);
} else if (!!vendor) {
tags.push(vendor);
}
return tags;

View file

@ -72,7 +72,7 @@ describe('<VulnerabilityFindingFlyout/>', () => {
</TestProvider>
);
getByText(mockVulnerabilityHit.vulnerability.data_source!.ID);
getByText('Elastic CSP');
getByText('Elastic');
getByText(moment(mockVulnerabilityHit.vulnerability.published_date).format('LL').toString());
getByText(mockVulnerabilityHit.vulnerability.description);
getAllByText(mockVulnerabilityHit.vulnerability?.cvss?.nvd?.V3Vector as string);
@ -93,13 +93,24 @@ describe('<VulnerabilityFindingFlyout/>', () => {
</TestProvider>
);
const dataSource = getByTestId(DATA_SOURCE_VULNERABILITY_FLYOUT);
const publisedDate = getByTestId(PUBLISHED_DATE_VULNERABILITY_FLYOUT);
const publishedDate = getByTestId(PUBLISHED_DATE_VULNERABILITY_FLYOUT);
const vulnerabilityScores = getByTestId(VULNERABILITY_SCORES_FLYOUT);
expect(dataSource.textContent).toEqual(`Data Source${EMPTY_VALUE}`);
expect(publisedDate.textContent).toEqual(`Published Date${EMPTY_VALUE}`);
expect(publishedDate.textContent).toEqual(`Published Date${EMPTY_VALUE}`);
expect(vulnerabilityScores.textContent).toEqual(`Vulnerability Scores${EMPTY_VALUE}`);
});
it('displays missing info callout when data source is not CSP', () => {
const { getByText } = render(<TestComponent vulnerabilityRecord={mockWizVulnerabilityHit} />);
getByText('Some fields not provided by Wiz');
});
it('does not display missing info callout when data source is CSP', () => {
const { queryByText } = render(<TestComponent vulnerabilityRecord={mockVulnerabilityHit} />);
const missingInfoCallout = queryByText('Some fields not provided by Wiz');
expect(missingInfoCallout).toBeNull();
});
it('show empty state for no fixes', () => {
const { getByText } = render(
<TestProvider>

View file

@ -28,6 +28,7 @@ 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 { 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';
@ -40,6 +41,7 @@ import {
} from '../test_subjects';
import { VulnerabilityTableTab } from './vulnerability_table_tab';
import { createDetectionRuleFromVulnerabilityFinding } from '../utils/create_detection_rule_from_vulnerability';
import { MissingFieldsCallout } from '../../configurations/findings_flyout/findings_flyout';
const overviewTabId = 'vuln-flyout-overview-tab';
const tableTabId = 'vuln-flyout-table-tab';
@ -232,6 +234,11 @@ export const VulnerabilityFindingFlyout = ({
isLoading={isLoading}
contentAriaLabel={LOADING_ARIA_LABEL}
>
{!isNativeCspFinding(vulnerabilityRecord) && selectedTabId === overviewTabId && (
<div style={{ marginBottom: euiThemeVars.euiSize }}>
<MissingFieldsCallout finding={vulnerabilityRecord} />
</div>
)}
{selectedTabContent}
</EuiSkeletonText>
</EuiFlyoutBody>

View file

@ -24,12 +24,11 @@ import {
CspVulnerabilityFinding,
} from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import { METRIC_TYPE } from '@kbn/analytics';
import {
VULNERABILITIES_FLYOUT_VISITS,
uiMetricService,
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import { getDatasetDisplayName } from '../../../common/utils/get_dataset_display_name';
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';
@ -287,11 +286,11 @@ export const VulnerabilityOverviewTab = ({ vulnerabilityRecord }: VulnerabilityT
<EuiFlexItem>
<h4 css={flyoutSubheadingStyle}>
<FormattedMessage
id="xpack.csp.vulnerabilities.vulnerabilityOverviewTab.sourceTitle"
defaultMessage="Source"
id="xpack.csp.vulnerabilities.vulnerabilityOverviewTab.vendorTitle"
defaultMessage="Vendor"
/>
</h4>
{getDatasetDisplayName(vulnerabilityRecord.data_stream?.dataset) || EMPTY_VALUE}
{getVendorName(vulnerabilityRecord) || EMPTY_VALUE}
</EuiFlexItem>
<EuiFlexItem>

View file

@ -39,8 +39,8 @@ export const vulnerabilitiesTableFieldLabels: Record<string, string> = {
'xpack.csp.vulnerabilityFindings.vulnerabilityFindingsTable.vulnerabilityFindingsTableColumn.packageFixedVersionColumnLabel',
{ defaultMessage: 'Fix Version' }
),
'data_stream.dataset': i18n.translate(
'xpack.csp.vulnerabilityFindings.vulnerabilityFindingsTable.vulnerabilityFindingsTableColumn.sourceColumnLabel',
{ defaultMessage: 'Source' }
'observer.vendor': i18n.translate(
'xpack.csp.vulnerabilityFindings.vulnerabilityFindingsTable.vulnerabilityFindingsTableColumn.vendorColumnLabel',
{ defaultMessage: 'Vendor' }
),
} as const;