Skip to content

Commit

Permalink
[Cloud Posture] CSPM support - dashboard onboarding per policy templa…
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanSh authored and jennypavlova committed Jan 13, 2023
1 parent 82969d6 commit 92f6d9d
Show file tree
Hide file tree
Showing 35 changed files with 948 additions and 295 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/cloud_security_posture/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status';
export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats';
export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats/{policy_template}';
export const BENCHMARKS_ROUTE_PATH = '/internal/cloud_security_posture/benchmarks';

export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture';
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cloud_security_posture/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ interface BaseCspSetupStatus {
installedPackagePolicies: number;
healthyAgents: number;
isPluginInitialized: boolean;
installedPolicyTemplates: PosturePolicyTemplate[];
}

interface CspSetupNotInstalledStatus extends BaseCspSetupStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const getInputType = (inputType: string): string => {
// Get the last part of the input type, input type structure: cloudbeat/<benchmark_id>
return inputType.split('/')[1];
};
export const getCSPKuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`;

export const CSP_FLEET_PACKAGE_KUERY = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`;

export function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
* 2.0.
*/

export * from './use_compliance_dashboard_data_api';
export * from './use_stats_api';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useKibana } from '../hooks/use_kibana';
import { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types';
import { STATS_ROUTE_PATH } from '../../../common/constants';

// TODO: consolidate both hooks into one hook with a dynamic key
const getCspmStatsKey = ['csp_cspm_dashboard_stats'];
const getKspmStatsKey = ['csp_kspm_dashboard_stats'];

export const getStatsRoute = (policyTemplate: PosturePolicyTemplate) => {
return STATS_ROUTE_PATH.replace('{policy_template}', policyTemplate);
};

export const useCspmStatsApi = (
options: UseQueryOptions<unknown, unknown, ComplianceDashboardData, string[]>
) => {
const { http } = useKibana().services;
return useQuery(
getCspmStatsKey,
// TODO: CIS AWS - remove casting and use actual policy template instead of benchmark_id
() => http.get<ComplianceDashboardData>(getStatsRoute('cis_aws' as PosturePolicyTemplate)),
options
);
};

export const useKspmStatsApi = (
options: UseQueryOptions<unknown, unknown, ComplianceDashboardData, string[]>
) => {
const { http } = useKibana().services;
return useQuery(
getKspmStatsKey,
// TODO: CIS AWS - remove casting and use actual policy template
() => http.get<ComplianceDashboardData>(getStatsRoute('cis_k8s' as PosturePolicyTemplate)),
options
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
*/

import { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public';
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../../common/constants';
import type { PosturePolicyTemplate } from '../../../common/types';
import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration';
import { useKibana } from '../hooks/use_kibana';

export const useCISIntegrationLink = (): string | undefined => {
export const useCspIntegrationLink = (
policyTemplate: PosturePolicyTemplate
): string | undefined => {
const { http } = useKibana().services;
const cisIntegration = useCisKubernetesIntegration();

if (!cisIntegration.isSuccess) return;

const path = pagePathGetters
.add_integration_to_policy({
integration: CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
integration: policyTemplate,
pkgkey: pkgKeyFromPackageInfo({
name: cisIntegration.data.item.name,
version: cisIntegration.data.item.version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ 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 { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration';
import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link';

const chance = new Chance();

jest.mock('../common/api/use_setup_status_api');
jest.mock('../common/navigation/use_navigate_to_cis_integration');
jest.mock('../common/hooks/use_subscription_status');
jest.mock('../common/navigation/use_csp_integration_link');

describe('<CloudPosturePage />', () => {
beforeEach(() => {
Expand Down Expand Up @@ -146,7 +147,7 @@ describe('<CloudPosturePage />', () => {
data: { status: 'not-installed' },
})
);
(useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());

const children = chance.sentence();
renderCloudPosturePage({ children });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,32 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import type { UseQueryResult } from '@tanstack/react-query';
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import {
EuiButton,
EuiEmptyPrompt,
EuiImage,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
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 { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration';
import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link';

import noDataIllustration from '../assets/illustrations/no_data_illustration.svg';

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 DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data';
export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed';

Expand All @@ -45,49 +57,110 @@ export const isCommonError = (error: unknown): error is CommonError => {
return true;
};

const packageNotInstalledRenderer = (cisIntegrationLink?: string) => (
<FullSizeCenteredPage>
<NoDataPage
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
css={css`
max-width: 950px;
`}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle', {
defaultMessage: 'Install Integration to get started',
})}
solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
docsLink={'https://ela.st/kspm'}
logo={'logoSecurity'}
actions={{
elasticAgent: {
href: cisIntegrationLink,
isDisabled: !cisIntegrationLink,
title: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel', {
defaultMessage: 'Add a KSPM integration',
}),
description: (
export interface CspNoDataPageProps {
pageTitle: NoDataPageProps['pageTitle'];
docsLink: NoDataPageProps['docsLink'];
actionHref: NoDataPageProps['actions']['elasticAgent']['href'];
actionTitle: NoDataPageProps['actions']['elasticAgent']['title'];
actionDescription: NoDataPageProps['actions']['elasticAgent']['description'];
testId: string;
}

export const CspNoDataPage = ({
pageTitle,
docsLink,
actionHref,
actionTitle,
actionDescription,
testId,
}: CspNoDataPageProps) => (
<NoDataPage
data-test-subj={testId}
css={css`
max-width: 950px;
`}
pageTitle={pageTitle}
solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
docsLink={docsLink}
logo="logoSecurity"
actions={{
elasticAgent: {
href: actionHref,
isDisabled: !actionHref,
title: actionTitle,
description: actionDescription,
},
}}
/>
);

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.packageNotInstalled.description"
defaultMessage="Use our {integrationFullName} (KSPM) integration to measure your Kubernetes cluster setup against CIS recommendations."
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptTitle"
defaultMessage="Detect security misconfigurations in your cloud resources!"
/>
</h2>
}
layout="horizontal"
color="plain"
body={
<p>
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.promptDescription"
defaultMessage="Add the Cloud and or Kubernetes Security Posture Management (K/CSPM) integration to begin. {learnMore}."
values={{
integrationFullName: (
<EuiLink href="https://ela.st/kspm">
learnMore: (
// TODO: CIS AWS - replace link with general doc for both integartions
<EuiLink href="https://ela.st/getting-started-with-kspm">
<FormattedMessage
id="xpack.csp.cloudPosturePage.packageNotInstalled.integrationNameLabel"
defaultMessage="Kubernetes Security Posture Management"
id="xpack.csp.cloudPosturePage.packageNotInstalledRenderer.learnMoreTitle"
defaultMessage="Learn more about Cloud Security Posture"
/>
</EuiLink>
),
}}
/>
),
},
}}
/>
</FullSizeCenteredPage>
);
</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}>
Expand Down Expand Up @@ -172,7 +245,8 @@ export const CloudPosturePage = <TData, TError>({
}: CloudPosturePageProps<TData, TError>) => {
const subscriptionStatus = useSubscriptionStatus();
const getSetupStatus = useCspSetupStatusApi();
const cisIntegrationLink = useCISIntegrationLink();
const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE);
const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE);

const render = () => {
if (subscriptionStatus.isError) {
Expand All @@ -196,7 +270,7 @@ export const CloudPosturePage = <TData, TError>({
}

if (getSetupStatus.data.status === 'not-installed') {
return packageNotInstalledRenderer(cisIntegrationLink);
return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink });
}

if (!query) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
SUPPORTED_POLICY_TEMPLATES,
SUPPORTED_CLOUDBEAT_INPUTS,
} from '../../../common/constants';
import { type PostureInput, type PosturePolicyTemplate } from '../../../common/types';
import type { PostureInput, PosturePolicyTemplate } from '../../../common/types';
import { assert } from '../../../common/utils/helpers';
import { cloudPostureIntegrations } from '../../common/constants';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export const NO_FINDINGS_STATUS_TEST_SUBJ = {
INDEXING: 'status-api-indexing',
INDEX_TIMEOUT: 'status-api-index-timeout',
UNPRIVILEGED: 'status-api-unprivileged',
NO_FINDINGS: 'no-findings-found',
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import * as TEST_SUBJ from './test_subjects';
import { useCspBenchmarkIntegrations } from './use_csp_benchmark_integrations';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link';

jest.mock('./use_csp_benchmark_integrations');
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
jest.mock('../../common/navigation/use_csp_integration_link');

const chance = new Chance();

describe('<Benchmarks />', () => {
Expand All @@ -41,7 +42,7 @@ describe('<Benchmarks />', () => {
})
);

(useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
(useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
});

const renderBenchmarks = (
Expand Down
Loading

0 comments on commit 92f6d9d

Please sign in to comment.