mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cloud Security] 3P integrations callouts (#194362)
This commit is contained in:
parent
5be5e36cf4
commit
d3f3d34519
26 changed files with 277 additions and 100 deletions
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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',
|
||||
|
|
|
@ -135,6 +135,9 @@ export const generateCspFinding = (
|
|||
data_stream: {
|
||||
dataset: 'cloud_security_posture.findings',
|
||||
},
|
||||
observer: {
|
||||
vendor: 'Elastic',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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' },
|
||||
];
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue