[Cloud Posture] Subscription gating (#140894)

This commit is contained in:
Ari Aviran 2022-09-19 14:58:44 +03:00 committed by GitHub
parent 140f720710
commit 5a81f0b559
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 384 additions and 30 deletions

View file

@ -0,0 +1,44 @@
/*
* 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 type { LicenseType } from '@kbn/licensing-plugin/common/types';
import { isSubscriptionAllowed } from './subscription';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
const ON_PREM_ALLOWED_LICENSES: readonly LicenseType[] = ['enterprise', 'trial'];
const ON_PREM_NOT_ALLOWED_LICENSES: readonly LicenseType[] = ['basic', 'gold', 'platinum'];
const ALL_LICENSE_TYPES: readonly LicenseType[] = [
'standard',
...ON_PREM_NOT_ALLOWED_LICENSES,
...ON_PREM_NOT_ALLOWED_LICENSES,
];
describe('isSubscriptionAllowed', () => {
it('should allow any cloud subscription', () => {
const isCloudEnabled = true;
ALL_LICENSE_TYPES.forEach((licenseType) => {
const license = licenseMock.createLicense({ license: { type: licenseType } });
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy();
});
});
it('should allow enterprise and trial licenses for on-prem', () => {
const isCloudEnabled = false;
ON_PREM_ALLOWED_LICENSES.forEach((licenseType) => {
const license = licenseMock.createLicense({ license: { type: licenseType } });
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy();
});
});
it('should not allow enterprise and trial licenses for on-prem', () => {
const isCloudEnabled = false;
ON_PREM_NOT_ALLOWED_LICENSES.forEach((licenseType) => {
const license = licenseMock.createLicense({ license: { type: licenseType } });
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeFalsy();
});
});
});

View file

@ -0,0 +1,24 @@
/*
* 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 type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
import { PLUGIN_NAME } from '..';
const MINIMUM_NON_CLOUD_LICENSE_TYPE: LicenseType = 'enterprise';
export const isSubscriptionAllowed = (isCloudEnabled?: boolean, license?: ILicense): boolean => {
if (isCloudEnabled) {
return true;
}
if (!license) {
return false;
}
const licenseCheck = license.check(PLUGIN_NAME, MINIMUM_NON_CLOUD_LICENSE_TYPE);
return licenseCheck.state === 'valid';
};

View file

@ -10,6 +10,17 @@
"description": "The cloud security posture plugin",
"server": true,
"ui": true,
"requiredPlugins": ["navigation", "data", "fleet", "unifiedSearch", "taskManager", "security", "charts", "discover"],
"requiredPlugins": [
"navigation",
"data",
"fleet",
"unifiedSearch",
"taskManager",
"security",
"charts",
"discover",
"cloud",
"licensing"
],
"requiredBundles": ["kibanaReact"]
}

View file

@ -0,0 +1,16 @@
/*
* 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 { createContext } from 'react';
interface SetupContextValue {
isCloudEnabled?: boolean;
}
/**
* A utility to pass data from the plugin setup lifecycle stage to application components
*/
export const SetupContext = createContext<SetupContextValue>({});

View file

@ -0,0 +1,22 @@
/*
* 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 { useContext } from 'react';
import { useQuery } from '@tanstack/react-query';
import { SetupContext } from '../../application/setup_context';
import { isSubscriptionAllowed } from '../../../common/utils/subscription';
import { useKibana } from './use_kibana';
const SUBSCRIPTION_QUERY_KEY = 'csp_subscription_query_key';
export const useSubscriptionStatus = () => {
const { licensing } = useKibana().services;
const { isCloudEnabled } = useContext(SetupContext);
return useQuery([SUBSCRIPTION_QUERY_KEY], async () => {
const license = await licensing.refresh();
return isSubscriptionAllowed(isCloudEnabled, license);
});
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { useSubscriptionStatus } from '../common/hooks/use_subscription_status';
import Chance from 'chance';
import {
DEFAULT_NO_DATA_TEST_SUBJECT,
@ -12,6 +13,7 @@ import {
isCommonError,
LOADING_STATE_TEST_SUBJECT,
PACKAGE_NOT_INSTALLED_TEST_SUBJECT,
SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT,
} from './cloud_posture_page';
import { createReactQueryResponse } from '../test/fixtures/react_query';
import { TestProvider } from '../test/test_provider';
@ -27,6 +29,7 @@ import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_
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');
describe('<CloudPosturePage />', () => {
beforeEach(() => {
@ -37,6 +40,13 @@ describe('<CloudPosturePage />', () => {
data: { status: 'indexed' },
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
});
const renderCloudPosturePage = (
@ -69,10 +79,66 @@ describe('<CloudPosturePage />', () => {
expect(screen.getByText(children)).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();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading state when the subscription query is loading', () => {
(useSubscriptionStatus 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 subscription query has an error', () => {
(useSubscriptionStatus 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 subscription not allowed prompt if subscription is not installed', () => {
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument();
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({
@ -88,6 +154,7 @@ describe('<CloudPosturePage />', () => {
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();
});
@ -105,6 +172,7 @@ describe('<CloudPosturePage />', () => {
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();
});
@ -122,6 +190,7 @@ describe('<CloudPosturePage />', () => {
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();
});
@ -135,6 +204,7 @@ describe('<CloudPosturePage />', () => {
renderCloudPosturePage({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
@ -149,6 +219,7 @@ describe('<CloudPosturePage />', () => {
renderCloudPosturePage({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
@ -177,6 +248,7 @@ describe('<CloudPosturePage />', () => {
expect(screen.getByText(text, { exact: false })).toBeInTheDocument()
);
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
@ -209,6 +281,7 @@ describe('<CloudPosturePage />', () => {
[error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument());
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.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();
});
@ -230,6 +303,7 @@ describe('<CloudPosturePage />', () => {
expect(screen.getByText(loading)).toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.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();
});
@ -245,6 +319,7 @@ describe('<CloudPosturePage />', () => {
expect(screen.getByTestId(DEFAULT_NO_DATA_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(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
@ -273,6 +348,7 @@ describe('<CloudPosturePage />', () => {
expect(screen.getByText(pageTitle)).toBeInTheDocument();
expect(screen.getAllByText(solution, { exact: false })[0]).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(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();

View file

@ -11,6 +11,8 @@ import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
import { css } from '@emotion/react';
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';
@ -20,6 +22,7 @@ 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 DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data';
export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed';
interface CommonError {
body: {
@ -120,28 +123,29 @@ const defaultErrorRenderer = (error: unknown) => (
</FullSizeCenteredPage>
);
const defaultNoDataRenderer = () => {
return (
const defaultNoDataRenderer = () => (
<FullSizeCenteredPage>
<NoDataPage
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.pageTitle', {
defaultMessage: 'No data found',
})}
solution={i18n.translate(
'xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel',
{
solution={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
}
)}
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{}}
/>
</FullSizeCenteredPage>
);
};
);
const subscriptionNotAllowedRenderer = () => (
<FullSizeCenteredPage data-test-subj={SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT}>
<SubscriptionNotAllowed />
</FullSizeCenteredPage>
);
interface CloudPosturePageProps<TData, TError> {
children: React.ReactNode;
@ -158,10 +162,23 @@ export const CloudPosturePage = <TData, TError>({
errorRender = defaultErrorRenderer,
noDataRenderer = defaultNoDataRenderer,
}: CloudPosturePageProps<TData, TError>) => {
const subscriptionStatus = useSubscriptionStatus();
const getSetupStatus = useCspSetupStatusApi();
const cisIntegrationLink = useCISIntegrationLink();
const render = () => {
if (subscriptionStatus.isError) {
return defaultErrorRenderer(subscriptionStatus.error);
}
if (subscriptionStatus.isLoading) {
return defaultLoadingRenderer();
}
if (!subscriptionStatus.data) {
return subscriptionNotAllowedRenderer();
}
if (getSetupStatus.isError) {
return defaultErrorRenderer(getSetupStatus.error);
}

View file

@ -0,0 +1,52 @@
/*
* 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 { EuiEmptyPrompt, EuiPageSection, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { useKibana } from '../common/hooks/use_kibana';
export const SubscriptionNotAllowed = () => {
const { application } = useKibana().services;
return (
<EuiPageSection color="danger" alignment="center">
<EuiEmptyPrompt
iconType="alert"
title={
<h2>
<FormattedMessage
id="xpack.csp.subscriptionNotAllowed.promptTitle"
defaultMessage="Upgrade for subscription features"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.csp.subscriptionNotAllowed.promptDescription"
defaultMessage="To use these cloud security features, you must {link}."
values={{
link: (
<EuiLink
href={application.getUrlForApp('management', {
path: 'stack/license_management/home',
})}
>
<FormattedMessage
id="xpack.csp.subscriptionNotAllowed.promptLinkText"
defaultMessage="start a trial or upgrade your subscription"
/>
</EuiLink>
),
}}
/>
</p>
}
/>
</EuiPageSection>
);
};

View file

@ -15,10 +15,12 @@ import { Benchmarks } from './benchmarks';
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';
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');
const chance = new Chance();
@ -31,6 +33,14 @@ describe('<Benchmarks />', () => {
data: { status: 'indexed' },
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
});

View file

@ -12,6 +12,7 @@ import { render } from '@testing-library/react';
import { TestProvider } from '../../test/test_provider';
import { ComplianceDashboard } from '.';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { useComplianceDashboardDataApi } from '../../common/api/use_compliance_dashboard_data_api';
import { DASHBOARD_CONTAINER } from './test_subjects';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
@ -22,6 +23,7 @@ import { expectIdsInDoc } from '../../test/utils';
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_compliance_dashboard_data_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
const chance = new Chance();
@ -179,6 +181,13 @@ describe('<ComplianceDashboard />', () => {
data: { status: 'indexed' },
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
});
const renderComplianceDashboardPage = () => {

View file

@ -20,6 +20,7 @@ import type { DataView } from '@kbn/data-plugin/common';
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 { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
@ -28,9 +29,11 @@ import { render } from '@testing-library/react';
import { useFindingsEsPit } from './es_pit/use_findings_es_pit';
import { expectIdsInDoc } from '../../test/utils';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
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_navigate_to_cis_integration');
jest.mock('./es_pit/use_findings_es_pit');
@ -46,6 +49,13 @@ beforeEach(() => {
setPitId: () => {},
pitIdRef: chance.guid(),
}));
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
});
const renderFindingsPage = () => {
@ -57,6 +67,7 @@ const renderFindingsPage = () => {
charts: chartPluginMock.createStartContract(),
discover: discoverPluginMock.createStartContract(),
fleet: fleetMock.createStartMock(),
licensing: licensingMock.createStart(),
}}
>
<Findings />

View file

@ -24,6 +24,7 @@ import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
jest.mock('../../../common/api/use_latest_findings_data_view');
jest.mock('../../../common/api/use_cis_kubernetes_integration');
@ -70,6 +71,7 @@ describe('<LatestFindingsContainer />', () => {
charts: chartPluginMock.createStartContract(),
discover: discoverPluginMock.createStartContract(),
fleet: fleetMock.createStartMock(),
licensing: licensingMock.createStart(),
}}
>
<FindingsEsPitContext.Provider value={{ setPitId, pitIdRef, pitQuery }}>

View file

@ -18,12 +18,14 @@ import * as TEST_SUBJECTS from './test_subjects';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { coreMock } from '@kbn/core/public/mocks';
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';
jest.mock('./use_csp_integration', () => ({
useCspIntegrationInfo: jest.fn(),
}));
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
const chance = new Chance();
@ -68,6 +70,14 @@ describe('<Rules />', () => {
data: { status: 'indexed' },
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
});

View file

@ -17,6 +17,7 @@ import type {
CspClientPluginStartDeps,
} from './types';
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants';
import { SetupContext } from './application/setup_context';
const LazyCspEditPolicy = lazy(() => import('./components/fleet_extensions/policy_extension_edit'));
const LazyCspCreatePolicy = lazy(
@ -42,10 +43,13 @@ export class CspPlugin
CspClientPluginStartDeps
>
{
private isCloudEnabled?: boolean;
public setup(
core: CoreSetup<CspClientPluginStartDeps, CspClientPluginStart>,
plugins: CspClientPluginSetupDeps
): CspClientPluginSetup {
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
// Return methods that should be available to other plugins
return {};
}
@ -74,7 +78,9 @@ export class CspPlugin
(
<KibanaContextProvider services={{ ...core, ...plugins }}>
<RedirectAppLinks coreStart={core}>
<SetupContext.Provider value={{ isCloudEnabled: this.isCloudEnabled }}>
<CspRouter {...props} />
</SetupContext.Provider>
</RedirectAppLinks>
</KibanaContextProvider>
),

View file

@ -17,6 +17,7 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import type { CspClientPluginStartDeps } from '../types';
interface CspAppDeps {
@ -33,6 +34,7 @@ export const TestProvider: React.FC<Partial<CspAppDeps>> = ({
charts: chartPluginMock.createStartContract(),
discover: discoverPluginMock.createStartContract(),
fleet: fleetMock.createStartMock(),
licensing: licensingMock.createStart(),
},
params = coreMock.createAppMountParameters(),
children,

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { ComponentType, ReactNode } from 'react';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
@ -32,6 +34,7 @@ export interface CspClientPluginSetupDeps {
// required
data: DataPublicPluginSetup;
fleet: FleetSetup;
cloud: CloudSetup;
// optional
}
@ -42,6 +45,7 @@ export interface CspClientPluginStartDeps {
charts: ChartsPluginStart;
discover: DiscoverStart;
fleet: FleetStart;
licensing: LicensingPluginStart;
// optional
}

View file

@ -20,6 +20,7 @@ import {
import { DeepReadonly } from 'utility-types';
import { createCspRuleSearchFilterByPackagePolicy } from '../../common/utils/helpers';
import {
CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
CLOUDBEAT_VANILLA,
CSP_RULE_SAVED_OBJECT_TYPE,
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
@ -127,6 +128,9 @@ export const isCspPackageInstalled = async (
}
};
export const isCspPackage = (packageName?: string) =>
packageName === CLOUD_SECURITY_POSTURE_PACKAGE_NAME;
const generateRulesFromTemplates = (
packagePolicyId: string,
policyId: string,

View file

@ -41,6 +41,7 @@ import {
SavedObjectsClientContract,
} from '@kbn/core/server';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
const chance = new Chance();
@ -76,6 +77,7 @@ describe('Cloud Security Posture Plugin', () => {
data: dataPluginMock.createStartContract(),
taskManager: taskManagerMock.createStart(),
security: securityMock.createStart(),
licensing: licensingMock.createStart(),
};
const contextMock = coreMock.createCustomRequestHandlerContext(mockRouteContext);

View file

@ -14,12 +14,17 @@ import type {
Plugin,
Logger,
} from '@kbn/core/server';
import { DeepReadonly } from 'utility-types';
import { DeletePackagePoliciesResponse, PackagePolicy } from '@kbn/fleet-plugin/common';
import {
import type { DeepReadonly } from 'utility-types';
import type {
DeletePackagePoliciesResponse,
PackagePolicy,
NewPackagePolicy,
} from '@kbn/fleet-plugin/common';
import type {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import { isSubscriptionAllowed } from '../common/utils/subscription';
import type {
CspServerPluginSetup,
CspServerPluginStart,
@ -32,6 +37,7 @@ import { setupSavedObjects } from './saved_objects';
import { initializeCspIndices } from './create_indices/create_indices';
import { initializeCspTransforms } from './create_transforms/create_transforms';
import {
isCspPackage,
isCspPackageInstalled,
onPackagePolicyPostCreateCallback,
removeCspRulesInstancesCallback,
@ -41,7 +47,6 @@ import {
updatePackagePolicyRuntimeCfgVar,
getCspRulesSO,
} from './routes/configuration/update_rules_configuration';
import {
removeFindingsStatsTask,
scheduleFindingsStatsTask,
@ -58,6 +63,7 @@ export class CspPlugin
>
{
private readonly logger: Logger;
private isCloudEnabled?: boolean;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
@ -77,6 +83,8 @@ export class CspPlugin
const coreStartServices = core.getStartServices();
this.setupCspTasks(plugins.taskManager, coreStartServices, this.logger);
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
return {};
}
@ -92,6 +100,26 @@ export class CspPlugin
this.initialize(core, plugins.taskManager);
}
plugins.fleet.registerExternalCallback(
'packagePolicyCreate',
async (
packagePolicy: NewPackagePolicy,
_context: RequestHandlerContext,
_request: KibanaRequest
): Promise<NewPackagePolicy> => {
const license = await plugins.licensing.refresh();
if (isCspPackage(packagePolicy.package?.name)) {
if (!isSubscriptionAllowed(this.isCloudEnabled, license)) {
throw new Error(
'To use this feature you must upgrade your subscription or start a trial'
);
}
}
return packagePolicy;
}
);
plugins.fleet.registerExternalCallback(
'packagePolicyPostCreate',
async (
@ -99,7 +127,7 @@ export class CspPlugin
context: RequestHandlerContext,
request: KibanaRequest
): Promise<PackagePolicy> => {
if (packagePolicy.package?.name === CLOUD_SECURITY_POSTURE_PACKAGE_NAME) {
if (isCspPackage(packagePolicy.package?.name)) {
await this.initialize(core, plugins.taskManager);
const soClient = (await context.core).savedObjects.client;
@ -128,7 +156,7 @@ export class CspPlugin
'postPackagePolicyDelete',
async (deletedPackagePolicies: DeepReadonly<DeletePackagePoliciesResponse>) => {
for (const deletedPackagePolicy of deletedPackagePolicies) {
if (deletedPackagePolicy.package?.name === CLOUD_SECURITY_POSTURE_PACKAGE_NAME) {
if (isCspPackage(deletedPackagePolicy.package?.name)) {
const soClient = core.savedObjects.createInternalRepository();
await removeCspRulesInstancesCallback(deletedPackagePolicy, soClient, this.logger);

View file

@ -4,10 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type {
PluginSetup as DataPluginSetup,
PluginStart as DataPluginStart,
} from '@kbn/data-plugin/server';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import {
TaskManagerSetupContract,
TaskManagerStartContract,
@ -42,6 +44,7 @@ export interface CspServerPluginSetupDeps {
data: DataPluginSetup;
taskManager: TaskManagerSetupContract;
security: SecurityPluginSetup;
cloud: CloudSetup;
// optional
}
@ -51,6 +54,7 @@ export interface CspServerPluginStartDeps {
fleet: FleetStartContract;
taskManager: TaskManagerStartContract;
security: SecurityPluginStart;
licensing: LicensingPluginStart;
}
export type CspServerPluginStartServices = Promise<