[Cloud Posture] onboarding prompts for vul_mgmt (#154118)

## Summary

This PR adds onboarding prompts for when vulnerability management
integration is not installed and when vulnerability management is
installed then show scanning empty prompt.

A `<NoVulnerabilitesState/>` component checks for `vuln_mgmt` status. If
the status is `not-deployed` or `indexing` then show
<ScanningVulnerabilitiesEmptyPrompt/>

The packageNoInstalledRender component will listen for the current tab
and renders on `useLocation().pathname`. If `currentTab` is
'vulnerabilities' then we show
`<VulnerabilitiesFindingsInstalledEmptyPrompt/>`. If `currentTab` is
`configurations` then show
`<ConfigurationFindingsInstalledEmptyPrompt/>`


Installed Prompt
<img width="1728" alt="Screen Shot 2023-03-30 at 3 45 35 PM"
src="https://user-images.githubusercontent.com/17135495/229010609-51a8f009-0fe1-45f3-8452-02bdd4234367.png">
Scanning Envioronment
<img width="1724" alt="Screen Shot 2023-03-30 at 3 45 20 PM"
src="https://user-images.githubusercontent.com/17135495/229010649-2d8c8482-c7d2-4040-927d-310354f99a94.png">

---------

Co-authored-by: Kfir Peled <kfir.peled@elastic.co>
This commit is contained in:
Lola 2023-04-12 14:55:21 -04:00 committed by GitHub
parent fb0c1bf400
commit b07743b269
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 853 additions and 455 deletions

View file

@ -12,7 +12,7 @@ export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats/{policy_
export const BENCHMARKS_ROUTE_PATH = '/internal/cloud_security_posture/benchmarks';
export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture';
// TODO: REMOVE CSP_LATEST_FINDINGS_DATA_VIEW and replace it with LATEST_FINDINGS_INDEX_PATTERN
export const CSP_LATEST_FINDINGS_DATA_VIEW = 'logs-cloud_security_posture.findings_latest-*';
export const FINDINGS_INDEX_NAME = 'logs-cloud_security_posture.findings';
@ -96,4 +96,7 @@ export const POSTURE_TYPES: { [x: string]: PostureTypes } = {
[CSPM_POLICY_TEMPLATE]: CSPM_POLICY_TEMPLATE,
[VULN_MGMT_POLICY_TEMPLATE]: VULN_MGMT_POLICY_TEMPLATE,
[POSTURE_TYPE_ALL]: POSTURE_TYPE_ALL,
} as const;
};
export const VULNERABILITIES = 'vulnerabilities';
export const CONFIGURATIONS = 'configurations';

View file

@ -87,6 +87,7 @@ export interface BaseCspSetupStatus {
kspm: BaseCspSetupBothPolicy;
vuln_mgmt: BaseCspSetupBothPolicy;
isPluginInitialized: boolean;
installedPackageVersion?: string | undefined;
}
export type CspSetupStatus = BaseCspSetupStatus;

View file

@ -8,25 +8,24 @@
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { DataView } from '@kbn/data-plugin/common';
import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../common/constants';
import { CspClientPluginStartDeps } from '../../types';
/**
* TODO: use perfected kibana data views
*/
export const useLatestFindingsDataView = () => {
export const useLatestFindingsDataView = (dataView: string) => {
const {
data: { dataViews },
} = useKibana<CspClientPluginStartDeps>().services;
const findDataView = async (): Promise<DataView> => {
const dataView = (await dataViews.find(CSP_LATEST_FINDINGS_DATA_VIEW))?.[0];
if (!dataView) {
throw new Error('Findings data view not found');
const dataViewObj = (await dataViews.find(dataView))?.[0];
if (!dataViewObj) {
throw new Error(`Data view not found [Name: {${dataView}}]`);
}
return dataView;
return dataViewObj;
};
return useQuery(['latest_findings_data_view'], findDataView);
return useQuery([`useDataView-${dataView}`], findDataView);
};

View file

@ -12,9 +12,9 @@ import { STATUS_ROUTE_PATH } from '../../../common/constants';
const getCspSetupStatusQueryKey = 'csp_status_key';
export const useCspSetupStatusApi = ({
options,
}: { options?: UseQueryOptions<CspSetupStatus, unknown, CspSetupStatus> } = {}) => {
export const useCspSetupStatusApi = (
options?: UseQueryOptions<CspSetupStatus, unknown, CspSetupStatus>
) => {
const { http } = useKibana().services;
return useQuery<CspSetupStatus, unknown, CspSetupStatus>(
[getCspSetupStatusQueryKey],

View file

@ -6,12 +6,12 @@
*/
import { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public';
import type { PosturePolicyTemplate } from '../../../common/types';
import type { CloudSecurityPolicyTemplate } from '../../../common/types';
import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration';
import { useKibana } from '../hooks/use_kibana';
export const useCspIntegrationLink = (
policyTemplate: PosturePolicyTemplate
policyTemplate: CloudSecurityPolicyTemplate
): string | undefined => {
const { http } = useKibana().services;
const cisIntegration = useCisKubernetesIntegration();

View file

@ -1,53 +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 { CspSetupStatus } from '../../../common/types';
// Cloud Posture Management Status
export const getCpmStatus = (cpmStatusData: CspSetupStatus | undefined) => {
// if has findings in any of the integrations.
const hasFindings =
cpmStatusData?.indicesDetails[0].status === 'not-empty' ||
cpmStatusData?.kspm.status === 'indexed' ||
cpmStatusData?.cspm.status === 'indexed';
// kspm
const hasKspmFindings =
cpmStatusData?.kspm?.status === 'indexed' ||
cpmStatusData?.indicesDetails[0].status === 'not-empty';
// cspm
const hasCspmFindings =
cpmStatusData?.cspm?.status === 'indexed' ||
cpmStatusData?.indicesDetails[0].status === 'not-empty';
const isKspmInstalled = cpmStatusData?.kspm?.status !== 'not-installed';
const isCspmInstalled = cpmStatusData?.cspm?.status !== 'not-installed';
const isKspmPrivileged = cpmStatusData?.kspm?.status !== 'unprivileged';
const isCspmPrivileged = cpmStatusData?.cspm?.status !== 'unprivileged';
const isCspmIntegrationInstalled = isCspmInstalled && isCspmPrivileged;
const isKspmIntegrationInstalled = isKspmInstalled && isKspmPrivileged;
const isEmptyData =
cpmStatusData?.kspm?.status === 'not-installed' &&
cpmStatusData?.cspm?.status === 'not-installed' &&
cpmStatusData?.indicesDetails[0].status === 'empty';
return {
hasFindings,
hasKspmFindings,
hasCspmFindings,
isCspmInstalled,
isKspmInstalled,
isKspmPrivileged,
isCspmPrivileged,
isCspmIntegrationInstalled,
isKspmIntegrationInstalled,
isEmptyData,
};
};

View file

@ -23,8 +23,6 @@ import React, { ComponentProps } from 'react';
import { UseQueryResult } from '@tanstack/react-query';
import { CloudPosturePage } from './cloud_posture_page';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
import { useCspSetupStatusApi } from '../common/api/use_setup_status_api';
import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link';
const chance = new Chance();
@ -35,19 +33,6 @@ jest.mock('../common/navigation/use_csp_integration_link');
describe('<CloudPosturePage />', () => {
beforeEach(() => {
jest.resetAllMocks();
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
cspm: { status: 'indexed' },
kspm: { status: 'indexed' },
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
],
},
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
@ -58,7 +43,9 @@ describe('<CloudPosturePage />', () => {
});
const renderCloudPosturePage = (
props: ComponentProps<typeof CloudPosturePage> = { children: null }
props: ComponentProps<typeof CloudPosturePage> = {
children: null,
}
) => {
const mockCore = coreMock.createStart();
@ -147,69 +134,6 @@ describe('<CloudPosturePage />', () => {
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders integrations installation prompt if integration is not installed', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
kspm: { status: 'not-installed' },
cspm: { status: 'not-installed' },
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
],
},
})
);
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading state when the integration query is loading', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default error state when the integration query has an error', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'error',
error: new Error('error'),
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading text when query isLoading', () => {
const query = createReactQueryResponse({
status: 'loading',

View file

@ -7,34 +7,22 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import type { UseQueryResult } from '@tanstack/react-query';
import {
EuiButton,
EuiEmptyPrompt,
EuiImage,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NoDataPage, NoDataPageProps } from '@kbn/kibana-react-plugin/public';
import { css } from '@emotion/react';
import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../common/constants';
import { SubscriptionNotAllowed } from './subscription_not_allowed';
import { useSubscriptionStatus } from '../common/hooks/use_subscription_status';
import { FullSizeCenteredPage } from './full_size_centered_page';
import { useCspSetupStatusApi } from '../common/api/use_setup_status_api';
import { CspLoadingState } from './csp_loading_state';
import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link';
import noDataIllustration from '../assets/illustrations/no_data_illustration.svg';
import { cspIntegrationDocsNavigation } from '../common/navigation/constants';
import { getCpmStatus } from '../common/utils/get_cpm_status';
export const LOADING_STATE_TEST_SUBJECT = 'cloud_posture_page_loading';
export const ERROR_STATE_TEST_SUBJECT = 'cloud_posture_page_error';
export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_package_not_installed';
export const CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_cspm_not_installed';
export const KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_kspm_not_installed';
export const VULN_MGMT_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT =
'cloud_posture_page_vuln_mgmt_not_installed';
export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data';
export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed';
@ -104,71 +92,6 @@ export const CspNoDataPage = ({
);
};
const packageNotInstalledRenderer = ({
kspmIntegrationLink,
cspmIntegrationLink,
}: {
kspmIntegrationLink?: string;
cspmIntegrationLink?: string;
}) => {
return (
<FullSizeCenteredPage>
<EuiEmptyPrompt
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
icon={<EuiImage size="fullWidth" src={noDataIllustration} alt="no-data-illustration" />}
title={
<h2>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptTitle"
defaultMessage="Detect security misconfigurations in your cloud infrastructure!"
/>
</h2>
}
layout="horizontal"
color="plain"
body={
<p>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptDescription"
defaultMessage="Detect and remediate potential configuration risks in your cloud infrastructure, like publicly accessible S3 buckets, with our Cloud and Kubernetes Security Posture Management solutions. {learnMore}"
values={{
learnMore: (
<EuiLink href={cspIntegrationDocsNavigation.cspm.overviewPath} target="_blank">
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.learnMoreTitle"
defaultMessage="Learn more about Cloud Security Posture"
/>
</EuiLink>
),
}}
/>
</p>
}
actions={
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill href={cspmIntegrationLink}>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.addCspmIntegrationButtonTitle"
defaultMessage="Add CSPM Integration"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill href={kspmIntegrationLink}>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.addKspmIntegrationButtonTitle"
defaultMessage="Add KSPM Integration"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
</FullSizeCenteredPage>
);
};
const defaultLoadingRenderer = () => (
<CspLoadingState data-test-subj={LOADING_STATE_TEST_SUBJECT}>
<FormattedMessage
@ -251,10 +174,6 @@ export const CloudPosturePage = <TData, TError>({
noDataRenderer = defaultNoDataRenderer,
}: CloudPosturePageProps<TData, TError>) => {
const subscriptionStatus = useSubscriptionStatus();
const { data: getSetupStatus, isLoading, isError, error } = useCspSetupStatusApi();
const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE);
const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE);
const { isEmptyData, hasFindings } = getCpmStatus(getSetupStatus);
const render = () => {
if (subscriptionStatus.isError) {
@ -269,23 +188,6 @@ export const CloudPosturePage = <TData, TError>({
return subscriptionNotAllowedRenderer();
}
if (isError) {
return defaultErrorRenderer(error);
}
if (isLoading) {
return defaultLoadingRenderer();
}
/* Checks if its a completely new user which means no integration has been installed and no latest findings default index has been found */
if (isEmptyData) {
return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink });
}
if (!hasFindings) {
return children;
}
if (!query) {
return children;
}

View file

@ -13,17 +13,24 @@ import {
EuiIcon,
EuiMarkdownFormat,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../common/constants';
import { FullSizeCenteredPage } from './full_size_centered_page';
import { useCspBenchmarkIntegrations } from '../pages/benchmarks/use_csp_benchmark_integrations';
import { useCISIntegrationPoliciesLink } from '../common/navigation/use_navigate_to_cis_integration_policies';
import { NO_FINDINGS_STATUS_TEST_SUBJ } from './test_subjects';
import { CloudPosturePage } from './cloud_posture_page';
import { CloudPosturePage, PACKAGE_NOT_INSTALLED_TEST_SUBJECT } from './cloud_posture_page';
import { useCspSetupStatusApi } from '../common/api/use_setup_status_api';
import type { IndexDetails, PostureTypes } from '../../common/types';
import { cspIntegrationDocsNavigation } from '../common/navigation/constants';
import noDataIllustration from '../assets/illustrations/no_data_illustration.svg';
import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link';
const REFETCH_INTERVAL_MS = 20000;
@ -172,18 +179,87 @@ const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] }
/>
);
const ConfigurationFindingsInstalledEmptyPrompt = ({
kspmIntegrationLink,
cspmIntegrationLink,
}: {
kspmIntegrationLink?: string;
cspmIntegrationLink?: string;
}) => {
return (
<EuiEmptyPrompt
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
icon={<EuiImage size="fullWidth" src={noDataIllustration} alt="no-data-illustration" />}
title={
<h2>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptTitle"
defaultMessage="Detect security misconfigurations in your cloud infrastructure!"
/>
</h2>
}
layout="horizontal"
color="plain"
body={
<p>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptDescription"
defaultMessage="Detect and remediate potential configuration risks in your cloud infrastructure, like publicly accessible S3 buckets, with our Cloud and Kubernetes Security Posture Management solutions. {learnMore}"
values={{
learnMore: (
<EuiLink href={cspIntegrationDocsNavigation.cspm.overviewPath} target="_blank">
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.learnMoreTitle"
defaultMessage="Learn more about Cloud Security Posture"
/>
</EuiLink>
),
}}
/>
</p>
}
actions={
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill href={cspmIntegrationLink}>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.addCspmIntegrationButtonTitle"
defaultMessage="Add CSPM Integration"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill href={kspmIntegrationLink}>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.addKspmIntegrationButtonTitle"
defaultMessage="Add KSPM Integration"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
);
};
/**
* This component will return the render states based on cloud posture setup status API
* since 'not-installed' is being checked globally by CloudPosturePage and 'indexed' is the pass condition, those states won't be handled here
* */
export const NoFindingsStates = ({ posturetype }: { posturetype: PostureTypes }) => {
const getSetupStatus = useCspSetupStatusApi({
options: { refetchInterval: REFETCH_INTERVAL_MS },
refetchInterval: REFETCH_INTERVAL_MS,
});
const statusKspm = getSetupStatus.data?.kspm?.status;
const statusCspm = getSetupStatus.data?.cspm?.status;
const indicesStatus = getSetupStatus.data?.indicesDetails;
const status = posturetype === 'cspm' ? statusCspm : statusKspm;
const showConfigurationInstallPrompt =
getSetupStatus.data?.kspm?.status === 'not-installed' &&
getSetupStatus.data?.cspm?.status === 'not-installed';
const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE);
const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE);
const unprivilegedIndices =
indicesStatus &&
indicesStatus
@ -196,6 +272,13 @@ export const NoFindingsStates = ({ posturetype }: { posturetype: PostureTypes })
if (status === 'index-timeout') return <IndexTimeout />; // agent added, index timeout has passed
if (status === 'unprivileged')
return <Unprivileged unprivilegedIndices={unprivilegedIndices || []} />; // user has no privileges for our indices
if (showConfigurationInstallPrompt)
return (
<ConfigurationFindingsInstalledEmptyPrompt
kspmIntegrationLink={kspmIntegrationLink}
cspmIntegrationLink={cspmIntegrationLink}
/>
);
};
return (

View file

@ -0,0 +1,227 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiLoadingLogo,
EuiEmptyPrompt,
EuiIcon,
EuiMarkdownFormat,
EuiLink,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
} from '@elastic/eui';
import { FormattedHTMLMessage, FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { VULN_MGMT_POLICY_TEMPLATE } from '../../common/constants';
import { FullSizeCenteredPage } from './full_size_centered_page';
import {
CloudPosturePage,
VULN_MGMT_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT,
} from './cloud_posture_page';
import { useCspSetupStatusApi } from '../common/api/use_setup_status_api';
import type { IndexDetails } from '../../common/types';
import { NO_VULNERABILITIES_STATUS_TEST_SUBJ } from './test_subjects';
import noDataIllustration from '../assets/illustrations/no_data_illustration.svg';
import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link';
const REFETCH_INTERVAL_MS = 20000;
const ScanningVulnerabilitiesEmptyPrompt = () => (
<EuiEmptyPrompt
data-test-subj={NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES}
color="plain"
icon={<EuiLoadingLogo logo="logoSecurity" size="xl" />}
title={
<h2>
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.scanningVulnerabilitiesEmptyPrompt.indexingButtonTitle"
defaultMessage="Scanning your cloud environment for vulnerabilities"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.scanningVulnerabilitiesEmptyPrompt.indexingDescription"
defaultMessage="Results should start to appear within a minute"
/>
</p>
}
/>
);
const VulnerabilitiesFindingsInstalledEmptyPrompt = ({
vulnMgmtIntegrationLink,
}: {
vulnMgmtIntegrationLink?: string;
}) => {
return (
<EuiEmptyPrompt
data-test-subj={VULN_MGMT_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT}
icon={<EuiImage size="fullWidth" src={noDataIllustration} alt="no-data-illustration" />}
title={
<h2>
<FormattedHTMLMessage
tagName="h2"
id="xpack.csp.cloudPosturePage.vulnerabilitiesInstalledEmptyPrompt.promptTitle"
defaultMessage="Detect vulnerabilities <br/> in your cloud assets!"
/>
</h2>
}
layout="horizontal"
color="plain"
body={
<p>
<FormattedMessage
id="xpack.csp.cloudPosturePage.vulnerabilitiesInstalledEmptyPrompt.promptDescription"
defaultMessage="Add the Elastic Vulnerability Management Integration to begin. {learnMore}."
values={{
learnMore: (
<EuiLink href={''} target="_blank">
<FormattedMessage
id="xpack.csp.cloudPosturePage.vulnerabilitiesInstalledEmptyPrompt.learnMoreTitle"
defaultMessage="Learn more about Elastic Vulnerability Management(VM) for Cloud"
/>
</EuiLink>
),
}}
/>
</p>
}
actions={
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill href={vulnMgmtIntegrationLink}>
<FormattedMessage
id="xpack.csp.cloudPosturePage.vulnerabilitiesInstalledEmptyPrompt.addVulMngtIntegrationButtonTitle"
defaultMessage="Install Elastic Vulnerability Management for Cloud"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
);
};
const IndexTimeout = () => (
<EuiEmptyPrompt
data-test-subj={NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT}
color="plain"
icon={<EuiLoadingLogo logo="logoSecurity" size="xl" />}
title={
<h2>
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.indexTimeout.indexTimeoutTitle"
defaultMessage="Findings Delayed"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.indexTimeout.indexTimeoutDescription"
defaultMessage="Collecting findings is taking longer than expected, please review our {docs} or reach out to support"
values={{
docs: (
<EuiLink href="https://ela.st/findings" target="_blank">
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.indexTimeout.indexTimeoutDocLink"
defaultMessage="docs"
/>
</EuiLink>
),
}}
/>
</p>
}
/>
);
const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] }) => (
<EuiEmptyPrompt
data-test-subj={NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED}
color="plain"
icon={<EuiIcon type="logoSecurity" size="xl" />}
title={
<h2>
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.unprivileged.unprivilegedTitle"
defaultMessage="Privileges required"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.csp.noVulnerabilitiesStates.unprivileged.unprivilegedDescription"
defaultMessage="To view cloud posture data, you must update privileges. For more information, contact your Kibana administrator."
/>
</p>
}
footer={
<EuiMarkdownFormat
css={css`
text-align: initial;
`}
children={
i18n.translate(
'xpack.csp.noVulnerabilitiesStates.unprivileged.unprivilegedFooterMarkdown',
{
defaultMessage:
'Required Elasticsearch index privilege `read` for the following indices:',
}
) + unprivilegedIndices.map((idx) => `\n- \`${idx}\``)
}
/>
}
/>
);
/**
* This component will return the render states based on cloud posture setup status API
* since 'not-installed' is being checked globally by CloudPosturePage and 'indexed' is the pass condition, those states won't be handled here
* */
export const NoVulnerabilitiesStates = () => {
const getSetupStatus = useCspSetupStatusApi({
refetchInterval: REFETCH_INTERVAL_MS,
});
const vulnMgmtIntegrationLink = useCspIntegrationLink(VULN_MGMT_POLICY_TEMPLATE);
const status = getSetupStatus.data?.vuln_mgmt?.status;
const indicesStatus = getSetupStatus.data?.indicesDetails;
const unprivilegedIndices =
indicesStatus &&
indicesStatus
.filter((idxDetails) => idxDetails.status === 'unprivileged')
.map((idxDetails: IndexDetails) => idxDetails.index)
.sort((a, b) => a.localeCompare(b));
const render = () => {
if (status === 'not-deployed' || status === 'indexing')
return <ScanningVulnerabilitiesEmptyPrompt />; // integration installed, but no agents added
if (status === 'index-timeout') return <IndexTimeout />; // agent added, index timeout has passed
if (status === 'not-installed')
return (
<VulnerabilitiesFindingsInstalledEmptyPrompt
vulnMgmtIntegrationLink={vulnMgmtIntegrationLink}
/>
);
if (status === 'unprivileged')
return <Unprivileged unprivilegedIndices={unprivilegedIndices || []} />; // user has no privileges for our indices
};
return (
<CloudPosturePage query={getSetupStatus}>
<FullSizeCenteredPage>{render()}</FullSizeCenteredPage>
</CloudPosturePage>
);
};

View file

@ -19,3 +19,12 @@ export const NO_FINDINGS_STATUS_TEST_SUBJ = {
UNPRIVILEGED: 'status-api-unprivileged',
NO_FINDINGS: 'no-findings-found',
};
export const NO_VULNERABILITIES_STATUS_TEST_SUBJ = {
SCANNING_VULNERABILITIES: 'scanning-vulnerabilities-empty-prompt',
UNPRIVILEGED: 'status-api-vuln-mgmt-unprivileged',
INDEX_TIMEOUT: 'status-api-vuln-mgmt-index-timeout',
NO_VULNERABILITIES: 'no-vulnerabilities-vuln-mgmt-found',
};
export const VULNERABILITIES_CONTAINER_TEST_SUBJ = 'vulnerabilities_container';

View file

@ -42,7 +42,7 @@ describe('<ComplianceDashboard />', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: { status: 'indexed' },
data: { status: 'indexed', installedPackageVersion: '1.2.13' },
})
);
@ -94,6 +94,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'not-deployed', healthyAgents: 0, installedPackagePolicies: 1 },
cspm: { status: 'not-deployed', healthyAgents: 0, installedPackagePolicies: 1 },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -131,6 +132,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'indexing', healthyAgents: 1, installedPackagePolicies: 1 },
cspm: { status: 'indexing', healthyAgents: 1, installedPackagePolicies: 1 },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -168,6 +170,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'index-timeout', healthyAgents: 1, installedPackagePolicies: 1 },
cspm: { status: 'index-timeout', healthyAgents: 1, installedPackagePolicies: 1 },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -205,6 +208,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'unprivileged', healthyAgents: 1, installedPackagePolicies: 1 },
cspm: { status: 'unprivileged', healthyAgents: 1, installedPackagePolicies: 1 },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -242,6 +246,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'indexed' },
cspm: { status: 'indexed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
@ -280,6 +285,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'indexed' },
cspm: { status: 'not-installed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
@ -318,6 +324,7 @@ describe('<ComplianceDashboard />', () => {
status: 'success',
data: {
cspm: { status: 'indexed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
@ -356,6 +363,7 @@ describe('<ComplianceDashboard />', () => {
status: 'success',
data: {
cspm: { status: 'indexed', healthyAgents: 0, installedPackagePolicies: 1 },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
@ -395,6 +403,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'indexed', healthyAgents: 0, installedPackagePolicies: 1 },
cspm: { status: 'not-installed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -434,6 +443,7 @@ describe('<ComplianceDashboard />', () => {
data: {
cspm: { status: 'indexed' },
kspm: { status: 'indexed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
@ -473,6 +483,7 @@ describe('<ComplianceDashboard />', () => {
data: {
cspm: { status: 'indexed' },
kspm: { status: 'indexed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' },
@ -512,6 +523,7 @@ describe('<ComplianceDashboard />', () => {
data: {
kspm: { status: 'not-deployed', healthyAgents: 0, installedPackagePolicies: 1 },
cspm: { status: 'not-installed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -527,7 +539,7 @@ describe('<ComplianceDashboard />', () => {
(useCspmStatsApi as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: undefined,
data: { stats: { totalFindings: 0 } },
}));
renderComplianceDashboardPage();
@ -553,6 +565,7 @@ describe('<ComplianceDashboard />', () => {
data: {
cspm: { status: 'not-deployed' },
kspm: { status: 'not-installed' },
installedPackageVersion: '1.2.13',
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
@ -568,7 +581,7 @@ describe('<ComplianceDashboard />', () => {
(useKspmStatsApi as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: undefined,
data: { stats: { totalFindings: 0 } },
}));
renderComplianceDashboardPage();

View file

@ -12,7 +12,11 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects';
import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link';
import type { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types';
import type {
PosturePolicyTemplate,
ComplianceDashboardData,
BaseCspSetupStatus,
} from '../../../common/types';
import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title';
import {
CloudPosturePage,
@ -35,7 +39,6 @@ import { SummarySection } from './dashboard_sections/summary_section';
import { BenchmarksSection } from './dashboard_sections/benchmarks_section';
import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../../common/constants';
import { cspIntegrationDocsNavigation } from '../../common/navigation/constants';
import { getCpmStatus } from '../../common/utils/get_cpm_status';
const noDataOptions: Record<
PosturePolicyTemplate,
@ -177,144 +180,157 @@ const IntegrationPostureDashboard = ({
);
};
const getDefaultTab = (
pluginStatus?: BaseCspSetupStatus,
cspmStats?: ComplianceDashboardData,
kspmStats?: ComplianceDashboardData
) => {
const cspmTotalFindings = cspmStats?.stats.totalFindings;
const kspmTotalFindings = kspmStats?.stats.totalFindings;
const installedPolicyTemplatesCspm = pluginStatus?.cspm?.status;
const installedPolicyTemplatesKspm = pluginStatus?.kspm?.status;
let preferredDashboard = CSPM_POLICY_TEMPLATE;
// cspm has findings
if (!!cspmTotalFindings) {
preferredDashboard = CSPM_POLICY_TEMPLATE;
}
// kspm has findings
else if (!!kspmTotalFindings) {
preferredDashboard = KSPM_POLICY_TEMPLATE;
}
// cspm is installed
else if (
installedPolicyTemplatesCspm !== 'unprivileged' &&
installedPolicyTemplatesCspm !== 'not-installed'
) {
preferredDashboard = CSPM_POLICY_TEMPLATE;
}
// kspm is installed
else if (
installedPolicyTemplatesKspm !== 'unprivileged' &&
installedPolicyTemplatesKspm !== 'not-installed'
) {
preferredDashboard = KSPM_POLICY_TEMPLATE;
}
return preferredDashboard;
};
export const ComplianceDashboard = () => {
const [selectedTab, setSelectedTab] = useState(CSPM_POLICY_TEMPLATE);
const { data: getSetupStatus } = useCspSetupStatusApi();
const {
hasKspmFindings,
hasCspmFindings,
isKspmInstalled,
isCspmInstalled,
isCspmIntegrationInstalled,
isKspmIntegrationInstalled,
} = getCpmStatus(getSetupStatus);
const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE);
const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE);
const isCloudSecurityPostureInstalled = !!getSetupStatus?.installedPackageVersion;
const getCspmDashboardData = useCspmStatsApi({
enabled: hasCspmFindings,
enabled: isCloudSecurityPostureInstalled,
});
const getKspmDashboardData = useKspmStatsApi({
enabled: hasKspmFindings,
enabled: isCloudSecurityPostureInstalled,
});
useEffect(() => {
const cspmTotalFindings = getCspmDashboardData.data?.stats.totalFindings;
const kspmTotalFindings = getKspmDashboardData.data?.stats.totalFindings;
const installedPolicyTemplatesCspm = getSetupStatus?.cspm?.status;
const installedPolicyTemplatesKspm = getSetupStatus?.kspm?.status;
let preferredDashboard = CSPM_POLICY_TEMPLATE;
// cspm has findings
if (!!cspmTotalFindings) {
preferredDashboard = CSPM_POLICY_TEMPLATE;
}
// kspm has findings
else if (!!kspmTotalFindings) {
preferredDashboard = KSPM_POLICY_TEMPLATE;
}
// cspm is installed
else if (
installedPolicyTemplatesCspm !== 'unprivileged' &&
installedPolicyTemplatesCspm !== 'not-installed'
) {
preferredDashboard = CSPM_POLICY_TEMPLATE;
}
// kspm is installed
else if (
installedPolicyTemplatesKspm !== 'unprivileged' &&
installedPolicyTemplatesKspm !== 'not-installed'
) {
preferredDashboard = KSPM_POLICY_TEMPLATE;
}
const preferredDashboard = getDefaultTab(
getSetupStatus,
getCspmDashboardData.data,
getKspmDashboardData.data
);
setSelectedTab(preferredDashboard);
}, [
getCspmDashboardData.data,
getCspmDashboardData.data?.stats.totalFindings,
getKspmDashboardData.data,
getKspmDashboardData.data?.stats.totalFindings,
getSetupStatus,
getSetupStatus?.cspm?.status,
getSetupStatus?.kspm?.status,
]);
const tabs = useMemo(
() => [
{
label: i18n.translate('xpack.csp.dashboardTabs.cloudTab.tabTitle', {
defaultMessage: 'Cloud',
}),
'data-test-subj': CLOUD_DASHBOARD_TAB,
isSelected: selectedTab === CSPM_POLICY_TEMPLATE,
onClick: () => setSelectedTab(CSPM_POLICY_TEMPLATE),
content: (
<>
{hasCspmFindings || !isCspmInstalled ? (
<CloudPosturePage query={getCspmDashboardData}>
<div data-test-subj={CLOUD_DASHBOARD_CONTAINER}>
<IntegrationPostureDashboard
dashboardType={CSPM_POLICY_TEMPLATE}
complianceData={getCspmDashboardData.data}
notInstalledConfig={getNotInstalledConfig(
CSPM_POLICY_TEMPLATE,
cspmIntegrationLink
)}
isIntegrationInstalled={isCspmIntegrationInstalled}
/>
</div>
</CloudPosturePage>
) : (
<NoFindingsStates posturetype={'cspm'} />
)}
</>
),
},
{
label: i18n.translate('xpack.csp.dashboardTabs.kubernetesTab.tabTitle', {
defaultMessage: 'Kubernetes',
}),
'data-test-subj': KUBERNETES_DASHBOARD_TAB,
isSelected: selectedTab === KSPM_POLICY_TEMPLATE,
onClick: () => setSelectedTab(KSPM_POLICY_TEMPLATE),
content: (
<>
{hasKspmFindings || !isKspmInstalled ? (
<CloudPosturePage query={getKspmDashboardData}>
<div data-test-subj={KUBERNETES_DASHBOARD_CONTAINER}>
<IntegrationPostureDashboard
dashboardType={KSPM_POLICY_TEMPLATE}
complianceData={getKspmDashboardData.data}
notInstalledConfig={getNotInstalledConfig(
KSPM_POLICY_TEMPLATE,
kspmIntegrationLink
)}
isIntegrationInstalled={isKspmIntegrationInstalled}
/>
</div>
</CloudPosturePage>
) : (
<NoFindingsStates posturetype={'kspm'} />
)}
</>
),
},
],
() =>
isCloudSecurityPostureInstalled
? [
{
label: i18n.translate('xpack.csp.dashboardTabs.cloudTab.tabTitle', {
defaultMessage: 'Cloud',
}),
'data-test-subj': CLOUD_DASHBOARD_TAB,
isSelected: selectedTab === CSPM_POLICY_TEMPLATE,
onClick: () => setSelectedTab(CSPM_POLICY_TEMPLATE),
content: (
<>
{isCloudSecurityPostureInstalled &&
(getSetupStatus?.cspm?.status === 'indexed' ||
getSetupStatus?.cspm?.status === 'not-installed') ? (
<CloudPosturePage query={getCspmDashboardData}>
<div data-test-subj={CLOUD_DASHBOARD_CONTAINER}>
<IntegrationPostureDashboard
dashboardType={CSPM_POLICY_TEMPLATE}
complianceData={getCspmDashboardData.data}
notInstalledConfig={getNotInstalledConfig(
CSPM_POLICY_TEMPLATE,
cspmIntegrationLink
)}
isIntegrationInstalled={getSetupStatus?.cspm?.status !== 'not-installed'}
/>
</div>
</CloudPosturePage>
) : (
<NoFindingsStates posturetype={'cspm'} />
)}
</>
),
},
{
label: i18n.translate('xpack.csp.dashboardTabs.kubernetesTab.tabTitle', {
defaultMessage: 'Kubernetes',
}),
'data-test-subj': KUBERNETES_DASHBOARD_TAB,
isSelected: selectedTab === KSPM_POLICY_TEMPLATE,
onClick: () => setSelectedTab(KSPM_POLICY_TEMPLATE),
content: (
<>
{isCloudSecurityPostureInstalled &&
(getSetupStatus?.kspm?.status === 'indexed' ||
getSetupStatus?.kspm?.status === 'not-installed') ? (
<CloudPosturePage query={getKspmDashboardData}>
<div data-test-subj={KUBERNETES_DASHBOARD_CONTAINER}>
<IntegrationPostureDashboard
dashboardType={KSPM_POLICY_TEMPLATE}
complianceData={getKspmDashboardData.data}
notInstalledConfig={getNotInstalledConfig(
KSPM_POLICY_TEMPLATE,
kspmIntegrationLink
)}
isIntegrationInstalled={getSetupStatus?.kspm?.status !== 'not-installed'}
/>
</div>
</CloudPosturePage>
) : (
<NoFindingsStates posturetype={'kspm'} />
)}
</>
),
},
]
: [],
[
cspmIntegrationLink,
getCspmDashboardData,
getKspmDashboardData,
kspmIntegrationLink,
selectedTab,
hasCspmFindings,
hasKspmFindings,
isKspmIntegrationInstalled,
isCspmIntegrationInstalled,
isCspmInstalled,
isKspmInstalled,
isCloudSecurityPostureInstalled,
getSetupStatus?.cspm?.status,
getSetupStatus?.kspm?.status,
]
);
return (
<CloudPosturePage query={selectedTab === 'cspm' ? getCspmDashboardData : getKspmDashboardData}>
<CloudPosturePage>
<EuiPageHeader
bottomBorder
pageTitle={
@ -336,6 +352,7 @@ export const ComplianceDashboard = () => {
`}
>
{tabs.find((t) => t.isSelected)?.content}
{!isCloudSecurityPostureInstalled && <NoFindingsStates posturetype={'cspm'} />}
</div>
</CloudPosturePage>
);

View file

@ -29,6 +29,7 @@ import { render } from '@testing-library/react';
import { expectIdsInDoc } from '../../test/utils';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import { PACKAGE_NOT_INSTALLED_TEST_SUBJECT } from '../../components/cloud_posture_page';
jest.mock('../../common/api/use_latest_findings_data_view');
jest.mock('../../common/api/use_setup_status_api');
@ -222,4 +223,33 @@ describe('<Findings />', () => {
],
});
});
it('renders integrations installation prompt if integration is not installed', async () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
kspm: { status: 'not-installed' },
cspm: { status: 'not-installed' },
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
],
},
})
);
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
renderFindingsPage();
expectIdsInDoc({
be: [PACKAGE_NOT_INSTALLED_TEST_SUBJECT],
notToBe: [
TEST_SUBJECTS.LATEST_FINDINGS_CONTAINER,
NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT,
NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED,
NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING,
NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED,
],
});
});
});

View file

@ -8,6 +8,7 @@ import React from 'react';
import { Redirect, Switch, useLocation } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { LATEST_FINDINGS_INDEX_PATTERN } from '../../../common/constants';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { NoFindingsStates } from '../../components/no_findings_states';
import { CloudPosturePage } from '../../components/cloud_posture_page';
@ -15,17 +16,20 @@ import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_
import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants';
import { FindingsByResourceContainer } from './latest_findings_by_resource/findings_by_resource_container';
import { LatestFindingsContainer } from './latest_findings/latest_findings_container';
import { getCpmStatus } from '../../common/utils/get_cpm_status';
export const Configurations = () => {
const location = useLocation();
const dataViewQuery = useLatestFindingsDataView();
const dataViewQuery = useLatestFindingsDataView(LATEST_FINDINGS_INDEX_PATTERN);
const { data: getSetupStatus } = useCspSetupStatusApi();
const { hasFindings, isCspmInstalled } = getCpmStatus(getSetupStatus);
const hasConfigurationFindings =
getSetupStatus?.kspm.status === 'indexed' || getSetupStatus?.cspm.status === 'indexed';
const noFindingsForPostureType = isCspmInstalled ? 'cspm' : 'kspm';
// For now, when there are no findings we prompt first to install cspm, if it is already installed we will prompt to
// install kspm
const noFindingsForPostureType =
getSetupStatus?.cspm.status !== 'not-installed' ? 'cspm' : 'kspm';
if (!hasFindings) return <NoFindingsStates posturetype={noFindingsForPostureType} />;
if (!hasConfigurationFindings) return <NoFindingsStates posturetype={noFindingsForPostureType} />;
return (
<CloudPosturePage query={dataViewQuery}>

View file

@ -23,6 +23,7 @@ import { CSP_MOMENT_FORMAT } from '../../../common/constants';
import {
INTERNAL_FEATURE_FLAGS,
LATEST_FINDINGS_INDEX_DEFAULT_NS,
LATEST_FINDINGS_INDEX_PATTERN,
} from '../../../../common/constants';
import { useLatestFindingsDataView } from '../../../common/api/use_latest_findings_data_view';
import { useKibana } from '../../../common/hooks/use_kibana';
@ -151,7 +152,7 @@ export const OverviewTab = ({ data }: { data: CspFinding }) => {
const {
services: { discover },
} = useKibana();
const latestFindingsDataView = useLatestFindingsDataView();
const latestFindingsDataView = useLatestFindingsDataView(LATEST_FINDINGS_INDEX_PATTERN);
const discoverIndexLink = useMemo(
() =>

View file

@ -29,6 +29,8 @@ import { ErrorCallout } from '../configurations/layout/error_callout';
import { FindingsSearchBar } from '../configurations/layout/findings_search_bar';
import { useFilteredDataView } from '../../common/api/use_filtered_data_view';
import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges';
import { NoVulnerabilitiesStates } from '../../components/no_vulnerabilities_states';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
const getDefaultQuery = ({ query, filters }: any): any => ({
query,
@ -39,6 +41,9 @@ const getDefaultQuery = ({ query, filters }: any): any => ({
export const Vulnerabilities = () => {
const { data, isLoading, error } = useFilteredDataView(LATEST_VULNERABILITIES_INDEX_PATTERN);
const getSetupStatus = useCspSetupStatusApi();
if (getSetupStatus?.data?.vuln_mgmt.status !== 'indexed') return <NoVulnerabilitiesStates />;
if (error) {
return <ErrorCallout error={error as Error} />;

View file

@ -0,0 +1,202 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import Chance from 'chance';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { Vulnerabilities } from './vulnerabilities';
import {
LATEST_VULNERABILITIES_INDEX_DEFAULT_NS,
VULN_MGMT_POLICY_TEMPLATE,
} from '../../../common/constants';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { useCISIntegrationPoliciesLink } from '../../common/navigation/use_navigate_to_cis_integration_policies';
import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link';
import {
NO_VULNERABILITIES_STATUS_TEST_SUBJ,
VULNERABILITIES_CONTAINER_TEST_SUBJ,
} from '../../components/test_subjects';
import { render } from '@testing-library/react';
import { expectIdsInDoc } from '../../test/utils';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import { VULN_MGMT_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT } from '../../components/cloud_posture_page';
import { TestProvider } from '../../test/test_provider';
jest.mock('../../common/api/use_latest_findings_data_view');
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
jest.mock('../../common/navigation/use_csp_integration_link');
const chance = new Chance();
beforeEach(() => {
jest.restoreAllMocks();
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
});
const renderVulnerabilitiesPage = () => {
render(
<TestProvider
deps={{
data: dataPluginMock.createStartContract(),
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
discover: discoverPluginMock.createStartContract(),
fleet: fleetMock.createStartMock(),
licensing: licensingMock.createStart(),
}}
>
<Vulnerabilities />
</TestProvider>
);
};
describe('<Vulnerabilities />', () => {
it('No vulnerabilities state: not-deployed - shows NotDeployed instead of vulnerabilities ', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
[VULN_MGMT_POLICY_TEMPLATE]: { status: 'not-deployed' },
indicesDetails: [{ index: LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, status: 'empty' }],
},
})
);
(useCISIntegrationPoliciesLink as jest.Mock).mockImplementation(() => chance.url());
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
renderVulnerabilitiesPage();
expectIdsInDoc({
be: [NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES],
notToBe: [
VULNERABILITIES_CONTAINER_TEST_SUBJ,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED,
],
});
});
it('No vulnerabilities state: indexing - shows Indexing instead of vulnerabilities ', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
[VULN_MGMT_POLICY_TEMPLATE]: { status: 'indexing' },
indicesDetails: [{ index: LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, status: 'empty' }],
},
})
);
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
renderVulnerabilitiesPage();
expectIdsInDoc({
be: [NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES],
notToBe: [
VULNERABILITIES_CONTAINER_TEST_SUBJ,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED,
],
});
});
it('No vulnerabilities state: index-timeout - shows IndexTimeout instead of vulnerabilities ', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
[VULN_MGMT_POLICY_TEMPLATE]: { status: 'index-timeout' },
indicesDetails: [{ index: LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, status: 'empty' }],
},
})
);
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
renderVulnerabilitiesPage();
expectIdsInDoc({
be: [NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT],
notToBe: [
VULNERABILITIES_CONTAINER_TEST_SUBJ,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED,
],
});
});
it('No vulnerabilities state: unprivileged - shows Unprivileged instead of vulnerabilities ', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
[VULN_MGMT_POLICY_TEMPLATE]: { status: 'unprivileged' },
indicesDetails: [{ index: LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, status: 'empty' }],
},
})
);
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
renderVulnerabilitiesPage();
expectIdsInDoc({
be: [NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED],
notToBe: [
VULNERABILITIES_CONTAINER_TEST_SUBJ,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT,
],
});
});
xit("renders the success state component when 'latest vulnerabilities findings' DataView exists and request status is 'success'", async () => {
// TODO: Add test cases for VulnerabilityContent
});
it('renders vuln_mgmt integrations installation prompt if vuln_mgmt integration is not installed', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: {
kspm: { status: 'not-deployed' },
cspm: { status: 'not-deployed' },
[VULN_MGMT_POLICY_TEMPLATE]: { status: 'not-installed' },
indicesDetails: [
{ index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' },
{ index: 'logs-cloud_security_posture.findings-default*', status: 'empty' },
{ index: LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, status: 'empty' },
],
},
})
);
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
renderVulnerabilitiesPage();
expectIdsInDoc({
be: [VULN_MGMT_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT],
notToBe: [
VULNERABILITIES_CONTAINER_TEST_SUBJ,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT,
NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED,
],
});
});
});

View file

@ -5,16 +5,16 @@
* 2.0.
*/
import { calculateCspStatusCode } from './status';
import { calculateIntegrationStatus } from './status';
import { CSPM_POLICY_TEMPLATE, VULN_MGMT_POLICY_TEMPLATE } from '../../../common/constants';
describe('calculateCspStatusCode for cspm', () => {
describe('calculateIntegrationStatus for cspm', () => {
it('Verify status when there are no permission for cspm', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'unprivileged',
findings: 'unprivileged',
latest: 'unprivileged',
stream: 'unprivileged',
score: 'unprivileged',
},
1,
@ -26,11 +26,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are no findings, no healthy agents and no installed policy templates', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
0,
@ -42,11 +42,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are findings and installed policies but no healthy agents', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'not-empty',
latest: 'empty',
stream: 'empty',
score: 'not-empty',
},
0,
@ -58,11 +58,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are findings ,installed policies and healthy agents', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'not-empty',
findings: 'not-empty',
latest: 'not-empty',
stream: 'not-empty',
score: 'not-empty',
},
1,
@ -74,11 +74,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are no findings ,installed policies and no healthy agents', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
0,
@ -90,11 +90,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are installed policies, healthy agents and no findings', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
1,
@ -106,11 +106,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are installed policies, healthy agents and no findings and been more than 10 minutes', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
1,
@ -122,11 +122,11 @@ describe('calculateCspStatusCode for cspm', () => {
});
it('Verify status when there are installed policies, healthy agents past findings but no recent findings', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'not-empty',
latest: 'empty',
stream: 'not-empty',
score: 'not-empty',
},
1,
@ -138,13 +138,13 @@ describe('calculateCspStatusCode for cspm', () => {
});
});
describe('calculateCspStatusCode for vul_mgmt', () => {
describe('calculateIntegrationStatus for vul_mgmt', () => {
it('Verify status when there are no permission for vul_mgmt', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'unprivileged',
findings: 'unprivileged',
latest: 'unprivileged',
stream: 'unprivileged',
score: 'unprivileged',
},
1,
@ -156,11 +156,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are no vul_mgmt findings, no healthy agents and no installed policy templates', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
0,
@ -172,11 +172,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are vul_mgmt findings and installed policies but no healthy agents', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'not-empty',
latest: 'empty',
stream: 'empty',
score: 'not-empty',
},
0,
@ -188,11 +188,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are vul_mgmt findings ,installed policies and healthy agents', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'not-empty',
findings: 'not-empty',
latest: 'not-empty',
stream: 'not-empty',
score: 'not-empty',
},
1,
@ -204,11 +204,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are no vul_mgmt findings ,installed policies and no healthy agents', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
0,
@ -220,11 +220,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are installed policies, healthy agents and no vul_mgmt findings', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
1,
@ -236,11 +236,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are installed policies, healthy agents and no vul_mgmt findings and been more than 10 minutes', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'empty',
latest: 'empty',
stream: 'empty',
score: 'empty',
},
1,
@ -252,11 +252,11 @@ describe('calculateCspStatusCode for vul_mgmt', () => {
});
it('Verify status when there are installed policies, healthy agents past vul_mgmt findings but no recent findings', async () => {
const statusCode = calculateCspStatusCode(
const statusCode = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: 'empty',
findings: 'not-empty',
latest: 'empty',
stream: 'not-empty',
score: 'not-empty',
},
1,

View file

@ -87,11 +87,11 @@ const getHealthyAgents = async (
);
};
export const calculateCspStatusCode = (
postureType: PostureTypes,
export const calculateIntegrationStatus = (
integration: PostureTypes,
indicesStatus: {
findingsLatest: IndexStatus;
findings: IndexStatus;
latest: IndexStatus;
stream: IndexStatus;
score?: IndexStatus;
},
healthyAgents: number,
@ -99,27 +99,22 @@ export const calculateCspStatusCode = (
installedPolicyTemplates: string[]
): CspStatusCode => {
// We check privileges only for the relevant indices for our pages to appear
const postureTypeCheck: PostureTypes = POSTURE_TYPES[postureType];
if (indicesStatus.findingsLatest === 'unprivileged' || indicesStatus.score === 'unprivileged')
const postureTypeCheck: PostureTypes = POSTURE_TYPES[integration];
if (indicesStatus.latest === 'unprivileged' || indicesStatus.score === 'unprivileged')
return 'unprivileged';
if (indicesStatus.latest === 'not-empty') return 'indexed';
if (indicesStatus.stream === 'not-empty' && indicesStatus.latest === 'empty') return 'indexing';
if (!installedPolicyTemplates.includes(postureTypeCheck)) return 'not-installed';
if (healthyAgents === 0) return 'not-deployed';
if (
indicesStatus.findingsLatest === 'empty' &&
indicesStatus.findings === 'empty' &&
indicesStatus.latest === 'empty' &&
indicesStatus.stream === 'empty' &&
timeSinceInstallationInMinutes < INDEX_TIMEOUT_IN_MINUTES
)
return 'waiting_for_results';
if (
indicesStatus.findingsLatest === 'empty' &&
indicesStatus.findings === 'empty' &&
timeSinceInstallationInMinutes > INDEX_TIMEOUT_IN_MINUTES
)
return 'index-timeout';
if (indicesStatus.findingsLatest === 'empty') return 'indexing';
if (indicesStatus.findings === 'not-empty') return 'indexed';
throw new Error('Could not determine csp status');
return 'index-timeout';
};
const assertResponse = (resp: CspSetupStatus, logger: CspApiRequestHandlerContext['logger']) => {
@ -261,11 +256,11 @@ export const getCspStatus = async ({
},
];
const statusCspm = calculateCspStatusCode(
const statusCspm = calculateIntegrationStatus(
CSPM_POLICY_TEMPLATE,
{
findingsLatest: findingsLatestIndexStatusCspm,
findings: findingsIndexStatusCspm,
latest: findingsLatestIndexStatusCspm,
stream: findingsIndexStatusCspm,
score: scoreIndexStatusCspm,
},
healthyAgentsCspm,
@ -273,11 +268,11 @@ export const getCspStatus = async ({
installedPolicyTemplates
);
const statusKspm = calculateCspStatusCode(
const statusKspm = calculateIntegrationStatus(
KSPM_POLICY_TEMPLATE,
{
findingsLatest: findingsLatestIndexStatusKspm,
findings: findingsIndexStatusKspm,
latest: findingsLatestIndexStatusKspm,
stream: findingsIndexStatusKspm,
score: scoreIndexStatusKspm,
},
healthyAgentsKspm,
@ -285,18 +280,18 @@ export const getCspStatus = async ({
installedPolicyTemplates
);
const statusVulnMgmt = calculateCspStatusCode(
const statusVulnMgmt = calculateIntegrationStatus(
VULN_MGMT_POLICY_TEMPLATE,
{
findingsLatest: vulnerabilitiesLatestIndexStatus,
findings: vulnerabilitiesIndexStatus,
latest: vulnerabilitiesLatestIndexStatus,
stream: vulnerabilitiesIndexStatus,
},
healthyAgentsVulMgmt,
calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE),
installedPolicyTemplates
);
const statusResponseInfo = getStatusResponse({
const statusResponseInfo: CspSetupStatus = getStatusResponse({
statusCspm,
statusKspm,
statusVulnMgmt,
@ -311,9 +306,7 @@ export const getCspStatus = async ({
isPluginInitialized: isPluginInitialized(),
});
if ((statusCspm && statusKspm && statusVulnMgmt) === 'not-installed') return statusResponseInfo;
const response = {
const response: CspSetupStatus = {
...statusResponseInfo,
installedPackageVersion: installation?.install_version,
};

View file

@ -9,7 +9,8 @@ import expect from '@kbn/expect';
import type { FtrProviderContext } from '../ftr_provider_context';
// Defined in CSP plugin
const FINDINGS_INDEX = 'logs-cloud_security_posture.findings_latest-default';
const FINDINGS_INDEX = 'logs-cloud_security_posture.findings-default';
const FINDINGS_LATEST_INDEX = 'logs-cloud_security_posture.findings_latest-default';
export function FindingsPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
@ -33,17 +34,48 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider
});
const index = {
remove: () => es.indices.delete({ index: FINDINGS_INDEX, ignore_unavailable: true }),
remove: () =>
Promise.all([
es.deleteByQuery({
index: FINDINGS_INDEX,
query: {
match_all: {},
},
ignore_unavailable: true,
refresh: true,
}),
es.deleteByQuery({
index: FINDINGS_LATEST_INDEX,
query: {
match_all: {},
},
ignore_unavailable: true,
refresh: true,
}),
]),
add: async <T>(findingsMock: T[]) => {
await waitForPluginInitialized();
await Promise.all(
findingsMock.map((finding) =>
await Promise.all([
...findingsMock.map((finding) =>
es.index({
index: FINDINGS_INDEX,
body: finding,
body: {
...finding,
'@timestamp': new Date().toISOString(),
},
refresh: true,
})
)
);
),
...findingsMock.map((finding) =>
es.index({
index: FINDINGS_LATEST_INDEX,
body: {
...finding,
'@timestamp': new Date().toISOString(),
},
refresh: true,
})
),
]);
},
};
@ -183,6 +215,7 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider
resourceFindingsTable,
findingsByResourceTable,
index,
waitForPluginInitialized,
distributionBar,
};
}

View file

@ -109,8 +109,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
resourceFindingsTable = findings.resourceFindingsTable;
distributionBar = findings.distributionBar;
// Before we start any test we must wait for cloud_security_posture plugin to complete its initialization
await findings.waitForPluginInitialized();
// Prepare mocked findings
await findings.index.remove();
await findings.index.add(data);
await findings.navigateToLatestFindingsPage();
await retry.waitFor(
'Findings table to be loaded',