diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index c1adc5c806334..69b99bc9d58cb 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -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'; diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index 8bd1c86eac3f6..6d2a64eb3a01d 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -87,6 +87,7 @@ export interface BaseCspSetupStatus { kspm: BaseCspSetupBothPolicy; vuln_mgmt: BaseCspSetupBothPolicy; isPluginInitialized: boolean; + installedPackageVersion?: string | undefined; } export type CspSetupStatus = BaseCspSetupStatus; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts index e40a4c5a8e8c2..2ab22ff4dd092 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts @@ -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().services; const findDataView = async (): Promise => { - 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); }; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts index 31edc058dec52..99e277b30bb54 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts @@ -12,9 +12,9 @@ import { STATUS_ROUTE_PATH } from '../../../common/constants'; const getCspSetupStatusQueryKey = 'csp_status_key'; -export const useCspSetupStatusApi = ({ - options, -}: { options?: UseQueryOptions } = {}) => { +export const useCspSetupStatusApi = ( + options?: UseQueryOptions +) => { const { http } = useKibana().services; return useQuery( [getCspSetupStatusQueryKey], diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts index 8d6e0f6c38583..fb255a9545ad4 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts @@ -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(); diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx b/x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx deleted file mode 100644 index cf40e4d797fa2..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx +++ /dev/null @@ -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, - }; -}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx index 3785690532442..0304695994088 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx @@ -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('', () => { 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('', () => { }); const renderCloudPosturePage = ( - props: ComponentProps = { children: null } + props: ComponentProps = { + children: null, + } ) => { const mockCore = coreMock.createStart(); @@ -147,69 +134,6 @@ describe('', () => { 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', diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx index 24d0c15db32ca..7fb386ae16160 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx @@ -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 ( - - } - title={ -

- -

- } - layout="horizontal" - color="plain" - body={ -

- - - - ), - }} - /> -

- } - actions={ - - - - - - - - - - - - - } - /> -
- ); -}; - const defaultLoadingRenderer = () => ( ({ noDataRenderer = defaultNoDataRenderer, }: CloudPosturePageProps) => { 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 = ({ 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; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx index 0dd428417d51b..daed1b794ff63 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx @@ -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 ( + } + title={ +

+ +

+ } + layout="horizontal" + color="plain" + body={ +

+ + + + ), + }} + /> +

+ } + actions={ + + + + + + + + + + + + + } + /> + ); +}; + /** * 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 ; // agent added, index timeout has passed if (status === 'unprivileged') return ; // user has no privileges for our indices + if (showConfigurationInstallPrompt) + return ( + + ); }; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx new file mode 100644 index 0000000000000..9106f1486b3f7 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx @@ -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 = () => ( + } + title={ +

+ +

+ } + body={ +

+ +

+ } + /> +); + +const VulnerabilitiesFindingsInstalledEmptyPrompt = ({ + vulnMgmtIntegrationLink, +}: { + vulnMgmtIntegrationLink?: string; +}) => { + return ( + } + title={ +

+ +

+ } + layout="horizontal" + color="plain" + body={ +

+ + + + ), + }} + /> +

+ } + actions={ + + + + + + + + } + /> + ); +}; + +const IndexTimeout = () => ( + } + title={ +

+ +

+ } + body={ +

+ + + + ), + }} + /> +

+ } + /> +); + +const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] }) => ( + } + title={ +

+ +

+ } + body={ +

+ +

+ } + footer={ + `\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 ; // integration installed, but no agents added + if (status === 'index-timeout') return ; // agent added, index timeout has passed + if (status === 'not-installed') + return ( + + ); + if (status === 'unprivileged') + return ; // user has no privileges for our indices + }; + + return ( + + {render()} + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 95f3a83de9708..7b818971d597f 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -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'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index 277edb4909dac..6ef3a7e4f4fe1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -42,7 +42,7 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed' }, + data: { status: 'indexed', installedPackageVersion: '1.2.13' }, }) ); @@ -94,6 +94,7 @@ describe('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ isSuccess: true, isLoading: false, - data: undefined, + data: { stats: { totalFindings: 0 } }, })); renderComplianceDashboardPage(); @@ -553,6 +565,7 @@ describe('', () => { 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('', () => { (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ isSuccess: true, isLoading: false, - data: undefined, + data: { stats: { totalFindings: 0 } }, })); renderComplianceDashboardPage(); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 6939601dfd745..6c75891fc62af 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -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 ? ( - -
- -
-
- ) : ( - - )} - - ), - }, - { - 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 ? ( - -
- -
-
- ) : ( - - )} - - ), - }, - ], + () => + 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') ? ( + +
+ +
+
+ ) : ( + + )} + + ), + }, + { + 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') ? ( + +
+ +
+
+ ) : ( + + )} + + ), + }, + ] + : [], [ cspmIntegrationLink, getCspmDashboardData, getKspmDashboardData, kspmIntegrationLink, selectedTab, - hasCspmFindings, - hasKspmFindings, - isKspmIntegrationInstalled, - isCspmIntegrationInstalled, - isCspmInstalled, - isKspmInstalled, + isCloudSecurityPostureInstalled, + getSetupStatus?.cspm?.status, + getSetupStatus?.kspm?.status, ] ); return ( - + { `} > {tabs.find((t) => t.isSelected)?.content} + {!isCloudSecurityPostureInstalled && } ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx index dc2b8818ff442..ef2fdc1ca83b8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx @@ -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('', () => { ], }); }); + + 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, + ], + }); + }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx index 940a6e0ad0f83..53fa5dae6908d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx @@ -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 ; + if (!hasConfigurationFindings) return ; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx index a0c5d330d22a3..efc2b487d3f98 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx @@ -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( () => diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index 0609d86497928..174939a5e0f99 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -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 ; if (error) { return ; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilties.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilties.test.tsx new file mode 100644 index 0000000000000..f083eaf8796aa --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilties.test.tsx @@ -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( + + + + ); +}; + +describe('', () => { + 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, + ], + }); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts index 715044cc37c60..00835a9520c60 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts @@ -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, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index 8c1b708528b34..aa98b0e5e5bfd 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -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, }; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index fd06d58e37081..6bb3a78e44cb4 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -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 (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, }; } diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index 99a399db97398..8877bc8146804 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -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',