[Cloud Security] Removing license gate keeping and displaying the table when there are findings (#190285)

This commit is contained in:
Jordan 2024-08-19 20:13:34 +03:00 committed by GitHub
parent 4e31c9b976
commit d0c1349122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 396 additions and 300 deletions

View file

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

View file

@ -12,9 +12,10 @@ import { useKibana } from './use_kibana';
const SUBSCRIPTION_QUERY_KEY = 'csp_subscription_query_key';
export const useSubscriptionStatus = () => {
export const useIsSubscriptionStatusValid = () => {
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,7 +5,6 @@
* 2.0.
*/
import { useSubscriptionStatus } from '../common/hooks/use_subscription_status';
import Chance from 'chance';
import {
DEFAULT_NO_DATA_TEST_SUBJECT,
@ -13,7 +12,6 @@ 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';
@ -23,27 +21,17 @@ 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 { useLicenseManagementLocatorApi } from '../common/api/use_license_management_locator_api';
const chance = new Chance();
jest.mock('../common/api/use_setup_status_api');
jest.mock('../common/api/use_license_management_locator_api');
jest.mock('../common/hooks/use_subscription_status');
jest.mock('../common/hooks/use_is_subscription_status_valid');
jest.mock('../common/navigation/use_csp_integration_link');
describe('<CloudPosturePage />', () => {
beforeEach(() => {
jest.resetAllMocks();
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(undefined);
});
const renderCloudPosturePage = (
@ -72,101 +60,16 @@ describe('<CloudPosturePage />', () => {
);
};
it('renders with license url locator', () => {
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() => 'http://license-url');
renderCloudPosturePage();
expect(screen.getByTestId('has_locator')).toBeInTheDocument();
});
it('renders no license url locator', () => {
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(undefined);
renderCloudPosturePage();
expect(screen.getByTestId('no_locator')).toBeInTheDocument();
});
it('renders children if setup status is indexed', () => {
const children = chance.sentence();
renderCloudPosturePage({ children });
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 default loading text when query isLoading', () => {
const query = createReactQueryResponse({
status: 'loading',
@ -176,7 +79,6 @@ 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();
@ -191,7 +93,6 @@ 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();
@ -220,7 +121,6 @@ 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();
@ -253,7 +153,6 @@ 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();
});
@ -275,7 +174,6 @@ 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();
});
@ -291,7 +189,6 @@ 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();
@ -320,7 +217,6 @@ 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,8 +11,6 @@ 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 { SubscriptionNotAllowed } from './subscription_not_allowed';
import { useSubscriptionStatus } from '../common/hooks/use_subscription_status';
import { FullSizeCenteredPage } from './full_size_centered_page';
import { CspLoadingState } from './csp_loading_state';
@ -22,7 +20,6 @@ export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_package_no
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';
interface CommonError {
body: {
@ -150,12 +147,6 @@ export const defaultNoDataRenderer = () => (
</FullSizeCenteredPage>
);
const subscriptionNotAllowedRenderer = () => (
<FullSizeCenteredPage data-test-subj={SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT}>
<SubscriptionNotAllowed />
</FullSizeCenteredPage>
);
interface CloudPosturePageProps<TData, TError> {
children: React.ReactNode;
query?: UseQueryResult<TData, TError>;
@ -171,21 +162,7 @@ export const CloudPosturePage = <TData, TError>({
errorRender = defaultErrorRenderer,
noDataRenderer = defaultNoDataRenderer,
}: CloudPosturePageProps<TData, TError>) => {
const subscriptionStatus = useSubscriptionStatus();
const render = () => {
if (subscriptionStatus.isError) {
return defaultErrorRenderer(subscriptionStatus.error);
}
if (subscriptionStatus.isLoading) {
return defaultLoadingRenderer();
}
if (!subscriptionStatus.data) {
return subscriptionNotAllowedRenderer();
}
if (!query) {
return children;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import { render, waitFor, within } from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
CspPolicyTemplateForm,
@ -53,9 +53,12 @@ import {
GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ,
SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ,
SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ,
SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT,
} from '../test_subjects';
import { ExperimentalFeaturesService } from '@kbn/fleet-plugin/public/services';
import { createFleetTestRendererMock } from '@kbn/fleet-plugin/public/mock';
import { useIsSubscriptionStatusValid } from '../../common/hooks/use_is_subscription_status_valid';
import { useLicenseManagementLocatorApi } from '../../common/api/use_license_management_locator_api';
// mock useParams
jest.mock('react-router-dom', () => ({
@ -66,6 +69,8 @@ jest.mock('react-router-dom', () => ({
}));
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_package_policy_list');
jest.mock('../../common/hooks/use_is_subscription_status_valid');
jest.mock('../../common/api/use_license_management_locator_api');
jest.mock('@kbn/fleet-plugin/public/services/experimental_features');
const onChange = jest.fn();
@ -85,9 +90,11 @@ describe('<CspPolicyTemplateForm />', () => {
(useParams as jest.Mock).mockReturnValue({
integration: undefined,
});
mockedExperimentalFeaturesService.get.mockReturnValue({
secretsStorage: true,
} as any);
(usePackagePolicyList as jest.Mock).mockImplementation((packageName) =>
createReactQueryResponseWithRefetch({
status: 'success',
@ -96,13 +103,22 @@ describe('<CspPolicyTemplateForm />', () => {
},
})
);
onChange.mockClear();
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponseWithRefetch({
status: 'success',
data: { status: 'indexed', installedPackageVersion: '1.2.13' },
})
);
(useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
});
const WrappedComponent = ({
@ -145,6 +161,53 @@ describe('<CspPolicyTemplateForm />', () => {
);
};
it('shows license block if subscription is not allowed', () => {
(useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
const policy = getMockPolicyK8s();
const { rerender } = render(<WrappedComponent newPolicy={policy} />);
rerender(<WrappedComponent newPolicy={{ ...policy, namespace: 'some-namespace' }} />);
expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument();
});
it('license block renders with license url locator', () => {
(useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() => 'http://license-url');
const policy = getMockPolicyK8s();
const { rerender } = render(<WrappedComponent newPolicy={policy} />);
rerender(<WrappedComponent newPolicy={{ ...policy, namespace: 'some-namespace' }} />);
expect(screen.getByTestId('has_locator')).toBeInTheDocument();
});
it('license block renders without license url locator', () => {
(useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(undefined);
const policy = getMockPolicyK8s();
const { rerender } = render(<WrappedComponent newPolicy={policy} />);
rerender(<WrappedComponent newPolicy={{ ...policy, namespace: 'some-namespace' }} />);
expect(screen.getByTestId('no_locator')).toBeInTheDocument();
});
it('updates package policy namespace to default when it changes', () => {
const policy = getMockPolicyK8s();
const { rerender } = render(<WrappedComponent newPolicy={policy} />);

View file

@ -30,6 +30,8 @@ import type {
import { PackageInfo, PackagePolicy } from '@kbn/fleet-plugin/common';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { useIsSubscriptionStatusValid } from '../../common/hooks/use_is_subscription_status_valid';
import { SubscriptionNotAllowed } from '../subscription_not_allowed';
import { CspRadioGroupProps, RadioGroup } from './csp_boxed_radio_group';
import { assert } from '../../../common/utils/helpers';
import type { CloudSecurityPolicyTemplate, PostureInput } from '../../../common/types_old';
@ -67,6 +69,7 @@ import { SetupTechnologySelector } from './setup_technology_selector/setup_techn
import { useSetupTechnology } from './setup_technology_selector/use_setup_technology';
import { AZURE_CREDENTIALS_TYPE } from './azure_credentials_form/azure_credentials_form';
import { AWS_CREDENTIALS_TYPE } from './aws_credentials_form/aws_credentials_form';
import { useKibana } from '../../common/hooks/use_kibana';
const DEFAULT_INPUT_TYPE = {
kspm: CLOUDBEAT_VANILLA,
@ -537,6 +540,125 @@ const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) =
</div>
);
const useEnsureDefaultNamespace = ({
newPolicy,
input,
updatePolicy,
}: {
newPolicy: NewPackagePolicy;
input: NewPackagePolicyPostureInput;
updatePolicy: (policy: NewPackagePolicy) => void;
}) => {
useEffect(() => {
if (newPolicy.namespace === POSTURE_NAMESPACE) return;
const policy = { ...getPosturePolicy(newPolicy, input.type), namespace: POSTURE_NAMESPACE };
updatePolicy(policy);
}, [newPolicy, input, updatePolicy]);
};
const usePolicyTemplateInitialName = ({
isEditPage,
isLoading,
integration,
newPolicy,
packagePolicyList,
updatePolicy,
setCanFetchIntegration,
}: {
isEditPage: boolean;
isLoading: boolean;
integration: CloudSecurityPolicyTemplate | undefined;
newPolicy: NewPackagePolicy;
packagePolicyList: PackagePolicy[] | undefined;
updatePolicy: (policy: NewPackagePolicy) => void;
setCanFetchIntegration: (canFetch: boolean) => void;
}) => {
useEffect(() => {
if (!integration) return;
if (isEditPage) return;
if (isLoading) return;
const packagePolicyListByIntegration = packagePolicyList?.filter(
(policy) => policy?.vars?.posture?.value === integration
);
const currentIntegrationName = getMaxPackageName(integration, packagePolicyListByIntegration);
if (newPolicy.name === currentIntegrationName) {
return;
}
updatePolicy({
...newPolicy,
name: currentIntegrationName,
});
setCanFetchIntegration(false);
// since this useEffect should only run on initial mount updatePolicy and newPolicy shouldn't re-trigger it
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, integration, isEditPage, packagePolicyList]);
};
const getSelectedOption = (
options: NewPackagePolicyInput[],
policyTemplate: string = CSPM_POLICY_TEMPLATE
) => {
// Looks for the enabled deployment (aka input). By default, all inputs are disabled.
// Initial state when all inputs are disabled is to choose the first available of the relevant policyTemplate
// Default selected policy template is CSPM
const selectedOption =
options.find((i) => i.enabled) ||
options.find((i) => i.policy_template === policyTemplate) ||
options[0];
assert(selectedOption, 'Failed to determine selected option'); // We can't provide a default input without knowing the policy template
assert(isPostureInput(selectedOption), 'Unknown option: ' + selectedOption.type);
return selectedOption;
};
/**
* Update CloudFormation template and stack name in the Agent Policy
* based on the selected policy template
*/
const useCloudFormationTemplate = ({
packageInfo,
newPolicy,
updatePolicy,
}: {
packageInfo: PackageInfo;
newPolicy: NewPackagePolicy;
updatePolicy: (policy: NewPackagePolicy) => void;
}) => {
useEffect(() => {
const templateUrl = getVulnMgmtCloudFormationDefaultValue(packageInfo);
// If the template is not available, do not update the policy
if (templateUrl === '') return;
const checkCurrentTemplate = newPolicy?.inputs?.find(
(i: any) => i.type === CLOUDBEAT_VULN_MGMT_AWS
)?.config?.cloud_formation_template_url?.value;
// If the template is already set, do not update the policy
if (checkCurrentTemplate === templateUrl) return;
updatePolicy?.({
...newPolicy,
inputs: newPolicy.inputs.map((input) => {
if (input.type === CLOUDBEAT_VULN_MGMT_AWS) {
return {
...input,
config: { cloud_formation_template_url: { value: templateUrl } },
};
}
return input;
}),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newPolicy?.vars?.cloud_formation_template_url, newPolicy, packageInfo]);
};
export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensionComponentProps>(
({
newPolicy,
@ -553,7 +675,11 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
: undefined;
// Handling validation state
const [isValid, setIsValid] = useState(true);
const { cloud } = useKibana().services;
const isServerless = !!cloud.serverless.projectType;
const input = getSelectedOption(newPolicy.inputs, integration);
const getIsSubscriptionValid = useIsSubscriptionStatusValid();
const isSubscriptionValid = !!getIsSubscriptionValid.data;
const { isAgentlessAvailable, setupTechnology, updateSetupTechnology } = useSetupTechnology({
input,
isAgentlessEnabled,
@ -615,6 +741,7 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
},
[onChange, isValid]
);
/**
* - Updates policy inputs by user selection
* - Updates hidden policy vars
@ -652,6 +779,12 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
enabled: canFetchIntegration,
});
useEffect(() => {
if (!isServerless) {
setIsValid(isSubscriptionValid);
}
}, [isServerless, isSubscriptionValid]);
useEffect(() => {
if (isEditPage) return;
if (isLoading) return;
@ -726,6 +859,10 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
},
];
if (!isSubscriptionValid) {
return <SubscriptionNotAllowed />;
}
return (
<>
{isEditPage && <EditScreenStepTitle />}
@ -857,122 +994,3 @@ CspPolicyTemplateForm.displayName = 'CspPolicyTemplateForm';
// eslint-disable-next-line import/no-default-export
export { CspPolicyTemplateForm as default };
const useEnsureDefaultNamespace = ({
newPolicy,
input,
updatePolicy,
}: {
newPolicy: NewPackagePolicy;
input: NewPackagePolicyPostureInput;
updatePolicy: (policy: NewPackagePolicy) => void;
}) => {
useEffect(() => {
if (newPolicy.namespace === POSTURE_NAMESPACE) return;
const policy = { ...getPosturePolicy(newPolicy, input.type), namespace: POSTURE_NAMESPACE };
updatePolicy(policy);
}, [newPolicy, input, updatePolicy]);
};
const usePolicyTemplateInitialName = ({
isEditPage,
isLoading,
integration,
newPolicy,
packagePolicyList,
updatePolicy,
setCanFetchIntegration,
}: {
isEditPage: boolean;
isLoading: boolean;
integration: CloudSecurityPolicyTemplate | undefined;
newPolicy: NewPackagePolicy;
packagePolicyList: PackagePolicy[] | undefined;
updatePolicy: (policy: NewPackagePolicy) => void;
setCanFetchIntegration: (canFetch: boolean) => void;
}) => {
useEffect(() => {
if (!integration) return;
if (isEditPage) return;
if (isLoading) return;
const packagePolicyListByIntegration = packagePolicyList?.filter(
(policy) => policy?.vars?.posture?.value === integration
);
const currentIntegrationName = getMaxPackageName(integration, packagePolicyListByIntegration);
if (newPolicy.name === currentIntegrationName) {
return;
}
updatePolicy({
...newPolicy,
name: currentIntegrationName,
});
setCanFetchIntegration(false);
// since this useEffect should only run on initial mount updatePolicy and newPolicy shouldn't re-trigger it
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, integration, isEditPage, packagePolicyList]);
};
const getSelectedOption = (
options: NewPackagePolicyInput[],
policyTemplate: string = CSPM_POLICY_TEMPLATE
) => {
// Looks for the enabled deployment (aka input). By default, all inputs are disabled.
// Initial state when all inputs are disabled is to choose the first available of the relevant policyTemplate
// Default selected policy template is CSPM
const selectedOption =
options.find((i) => i.enabled) ||
options.find((i) => i.policy_template === policyTemplate) ||
options[0];
assert(selectedOption, 'Failed to determine selected option'); // We can't provide a default input without knowing the policy template
assert(isPostureInput(selectedOption), 'Unknown option: ' + selectedOption.type);
return selectedOption;
};
/**
* Update CloudFormation template and stack name in the Agent Policy
* based on the selected policy template
*/
const useCloudFormationTemplate = ({
packageInfo,
newPolicy,
updatePolicy,
}: {
packageInfo: PackageInfo;
newPolicy: NewPackagePolicy;
updatePolicy: (policy: NewPackagePolicy) => void;
}) => {
useEffect(() => {
const templateUrl = getVulnMgmtCloudFormationDefaultValue(packageInfo);
// If the template is not available, do not update the policy
if (templateUrl === '') return;
const checkCurrentTemplate = newPolicy?.inputs?.find(
(i: any) => i.type === CLOUDBEAT_VULN_MGMT_AWS
)?.config?.cloud_formation_template_url?.value;
// If the template is already set, do not update the policy
if (checkCurrentTemplate === templateUrl) return;
updatePolicy?.({
...newPolicy,
inputs: newPolicy.inputs.map((input) => {
if (input.type === CLOUDBEAT_VULN_MGMT_AWS) {
return {
...input,
config: { cloud_formation_template_url: { value: templateUrl } },
};
}
return input;
}),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newPolicy?.vars?.cloud_formation_template_url, newPolicy, packageInfo]);
};

View file

@ -8,20 +8,25 @@
import React from 'react';
import { EuiEmptyPrompt, EuiLink, EuiPageSection } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT } from './test_subjects';
import { useLicenseManagementLocatorApi } from '../common/api/use_license_management_locator_api';
export const SubscriptionNotAllowed = () => {
const handleNavigateToLicenseManagement = useLicenseManagementLocatorApi();
return (
<EuiPageSection color="danger" alignment="center">
<EuiPageSection
color="danger"
alignment="center"
data-test-subj={SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT}
>
<EuiEmptyPrompt
iconType="warning"
title={
<h2>
<FormattedMessage
id="xpack.csp.subscriptionNotAllowed.promptTitle"
defaultMessage="Upgrade for subscription features"
defaultMessage="Upgrade your subscription to an Enterprise license"
/>
</h2>
}

View file

@ -85,3 +85,5 @@ export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = {
CREDENTIALS_FILE: 'credentials_file_test_id',
CREDENTIALS_JSON: 'credentials_json_test_id',
};
export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed';

View file

@ -15,7 +15,6 @@ import { Benchmarks } from './benchmarks';
import * as TEST_SUBJ from './test_subjects';
import { useCspBenchmarkIntegrationsV2 } from './use_csp_benchmark_integrations';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link';
import { ERROR_STATE_TEST_SUBJECT } from './benchmarks_table';
import { useLicenseManagementLocatorApi } from '../../common/api/use_license_management_locator_api';
@ -23,7 +22,7 @@ import { useLicenseManagementLocatorApi } from '../../common/api/use_license_man
jest.mock('./use_csp_benchmark_integrations');
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_license_management_locator_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/hooks/use_is_subscription_status_valid');
jest.mock('../../common/navigation/use_csp_integration_link');
const chance = new Chance();
@ -45,13 +44,6 @@ describe('<Benchmarks />', () => {
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',

View file

@ -13,7 +13,6 @@ import { TestProvider } from '../../test/test_provider';
import { ComplianceDashboard, getDefaultTab } from '.';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useLicenseManagementLocatorApi } from '../../common/api/use_license_management_locator_api';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { useKspmStatsApi, useCspmStatsApi } from '../../common/api/use_stats_api';
import {
CLOUD_DASHBOARD_CONTAINER,
@ -43,7 +42,7 @@ import { MemoryRouter } from 'react-router-dom';
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_stats_api');
jest.mock('../../common/api/use_license_management_locator_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/hooks/use_is_subscription_status_valid');
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
jest.mock('../../common/navigation/use_csp_integration_link');
@ -58,18 +57,12 @@ describe('<ComplianceDashboard />', () => {
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useCspmStatsApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
})
);
(useKspmStatsApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',

View file

@ -44,7 +44,7 @@ describe('<Findings />', () => {
server.use(rulesGetStatesHandler);
});
it('renders integrations installation prompt if integration is not installed', async () => {
it('renders integrations installation prompt if integration is not installed and there are no findings', async () => {
server.use(statusHandlers.notInstalledHandler);
renderFindingsPage();
@ -53,6 +53,37 @@ describe('<Findings />', () => {
expect(screen.getByText(/add kspm integration/i)).toBeInTheDocument();
});
it("renders the 'latest misconfigurations findings' DataTable component when the CSPM/KSPM integration status is not installed but there are findings", async () => {
const finding1 = generateCspFinding('0003', 'failed');
const finding2 = generateCspFinding('0004', 'passed');
server.use(statusHandlers.notInstalledHasMisconfigurationsFindingsHandler);
server.use(bsearchFindingsHandler([finding1, finding2]));
renderFindingsPage();
// Loading while checking the status API and fetching the findings
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => expect(screen.getByText(/2 findings/i)).toBeInTheDocument());
const fieldsToCheck = [
finding1.resource.name,
finding1.resource.id,
finding1.rule.benchmark.rule_number as string,
finding1.rule.name,
finding1.rule.section,
finding2.resource.name,
finding2.resource.id,
finding2.rule.benchmark.rule_number as string,
finding2.rule.name,
finding2.rule.section,
];
fieldsToCheck.forEach((fieldValue) => {
expect(screen.getByText(fieldValue)).toBeInTheDocument();
});
});
it("renders the 'latest findings' DataTable component when the CSPM/KSPM integration status is 'indexed' grouped by 'none'", async () => {
const finding1 = generateCspFinding('0001', 'failed');
const finding2 = generateCspFinding('0002', 'passed');

View file

@ -21,8 +21,12 @@ export const Configurations = () => {
const location = useLocation();
const dataViewQuery = useDataView(CDR_MISCONFIGURATIONS_DATA_VIEW_ID_PREFIX);
const { data: getSetupStatus, isLoading: getSetupStatusIsLoading } = useCspSetupStatusApi();
const hasConfigurationFindings =
getSetupStatus?.kspm.status === 'indexed' || getSetupStatus?.cspm.status === 'indexed';
const hasMisconfigurationsFindings = !!getSetupStatus?.hasMisconfigurationsFindings;
const hasFindings =
hasMisconfigurationsFindings ||
getSetupStatus?.kspm.status === 'indexed' ||
getSetupStatus?.cspm.status === 'indexed';
// For now, when there are no findings we prompt first to install cspm, if it is already installed we will prompt to
// install kspm
@ -30,7 +34,7 @@ export const Configurations = () => {
getSetupStatus?.cspm.status !== 'not-installed' ? 'cspm' : 'kspm';
if (getSetupStatusIsLoading) return defaultLoadingRenderer();
if (!hasConfigurationFindings) return <NoFindingsStates postureType={noFindingsForPostureType} />;
if (!hasFindings) return <NoFindingsStates postureType={noFindingsForPostureType} />;
const dataViewContextValue = {
dataView: dataViewQuery.data!,

View file

@ -16,7 +16,6 @@ import { PageUrlParams } from '../../../common/types/latest';
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 { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link';
import { useLicenseManagementLocatorApi } from '../../common/api/use_license_management_locator_api';
import { useCspBenchmarkIntegrationsV2 } from '../benchmarks/use_csp_benchmark_integrations';
@ -24,7 +23,7 @@ import * as TEST_SUBJECTS from './test_subjects';
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_license_management_locator_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/hooks/use_is_subscription_status_valid');
jest.mock('../../common/navigation/use_csp_integration_link');
jest.mock('../benchmarks/use_csp_benchmark_integrations', () => ({
useCspBenchmarkIntegrationsV2: jest.fn(),
@ -81,13 +80,6 @@ describe('<Rules />', () => {
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',

View file

@ -14,7 +14,6 @@ import {
} from '../../../common/constants';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useDataView } from '../../common/api/use_data_view';
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';
@ -31,7 +30,7 @@ import { createStubDataView } from '@kbn/data-views-plugin/common/stubs';
jest.mock('../../common/api/use_data_view');
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_license_management_locator_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/hooks/use_is_subscription_status_valid');
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
jest.mock('../../common/navigation/use_csp_integration_link');
@ -40,13 +39,6 @@ const chance = new Chance();
beforeEach(() => {
jest.restoreAllMocks();
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',

View file

@ -15,7 +15,6 @@ import {
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';
@ -35,7 +34,7 @@ import { mockCnvmDashboardData } from './_mocks_/vulnerability_dashboard.mock';
jest.mock('../../common/api/use_data_view');
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_license_management_locator_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/hooks/use_is_subscription_status_valid');
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
jest.mock('../../common/navigation/use_csp_integration_link');
jest.mock('../../common/api/use_vulnerability_dashboard_api');
@ -45,13 +44,6 @@ const chance = new Chance();
beforeEach(() => {
jest.restoreAllMocks();
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',

View file

@ -69,6 +69,7 @@ export interface CspClientPluginStartDeps {
share: SharePluginStart;
storage: Storage;
spaces: SpacesPluginStart;
cloud: CloudSetup;
// optional
usageCollection?: UsageCollectionStart;

View file

@ -48,6 +48,47 @@ export const notInstalledHandler = http.get(STATUS_URL, () => {
});
});
export const notInstalledHasMisconfigurationsFindingsHandler = http.get(STATUS_URL, () => {
return HttpResponse.json({
hasMisconfigurationsFindings: true,
cspm: {
status: 'not-installed',
healthyAgents: 1,
installedPackagePolicies: 1,
},
kspm: {
status: 'not-installed',
healthyAgents: 1,
installedPackagePolicies: 1,
},
vuln_mgmt: {
status: 'not-installed',
healthyAgents: 1,
installedPackagePolicies: 1,
},
indicesDetails: [
{
index: 'logs-cloud_security_posture.findings_latest-default',
status: 'empty',
},
{
index: 'logs-cloud_security_posture.findings-default*',
status: 'empty',
},
{
index: 'logs-cloud_security_posture.scores-default',
status: 'empty',
},
{
index: 'logs-cloud_security_posture.vulnerabilities_latest-default',
status: 'empty',
},
],
isPluginInitialized: true,
latestPackageVersion: '1.9.0',
});
});
export const notDeployedHandler = http.get(STATUS_URL, () => {
return HttpResponse.json({
cspm: {

View file

@ -32,6 +32,7 @@ import {
POSTURE_TYPE_ALL,
LATEST_VULNERABILITIES_RETENTION_POLICY,
LATEST_FINDINGS_RETENTION_POLICY,
CDR_MISCONFIGURATIONS_INDEX_PATTERN,
} from '../../../common/constants';
import type {
CspApiRequestHandlerContext,
@ -142,6 +143,43 @@ const assertResponse = (resp: CspSetupStatus, logger: CspApiRequestHandlerContex
}
};
const checkIndexHasFindings = async (
esClient: ElasticsearchClient,
index: string,
retentionPolicy: string,
logger: Logger
) => {
try {
const response = await esClient.search({
index,
size: 0, // We only need to know if there are any hits, so we don't need to retrieve documents
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: `now-${retentionPolicy}`,
lte: 'now',
},
},
},
],
},
},
});
// Check the number of hits
const totalHits =
typeof response.hits.total === 'object' ? response.hits.total.value : response.hits.total;
return !!totalHits;
} catch (err) {
logger.error(`Error checking if index ${index} has findings`);
logger.error(err);
}
};
export const getCspStatus = async ({
logger,
esClient,
@ -153,6 +191,7 @@ export const getCspStatus = async ({
isPluginInitialized,
}: CspStatusDependencies): Promise<CspSetupStatus> => {
const [
hasMisconfigurationsFindings,
findingsLatestIndexStatus,
findingsIndexStatus,
scoreIndexStatus,
@ -171,6 +210,12 @@ export const getCspStatus = async ({
installedPackagePoliciesVulnMgmt,
installedPolicyTemplates,
] = await Promise.all([
checkIndexHasFindings(
esClient,
CDR_MISCONFIGURATIONS_INDEX_PATTERN,
LATEST_FINDINGS_RETENTION_POLICY,
logger
),
checkIndexStatus(esClient, LATEST_FINDINGS_INDEX_DEFAULT_NS, logger, {
postureType: POSTURE_TYPE_ALL,
retentionTime: LATEST_VULNERABILITIES_RETENTION_POLICY,
@ -357,6 +402,7 @@ export const getCspStatus = async ({
const response: CspSetupStatus = {
...statusResponseInfo,
installedPackageVersion: installation?.install_version,
hasMisconfigurationsFindings,
};
assertResponse(response, logger);

View file

@ -61,6 +61,55 @@ export default function (providerContext: FtrProviderContext) {
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
it(`Return hasMisconfigurationsFindings true when there are latest findings but no installed integrations`, async () => {
await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS);
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set('kbn-xsrf', 'xxxx')
.expect(200);
expect(res.hasMisconfigurationsFindings).to.eql(
true,
`expected hasMisconfigurationsFindings to be true but got ${res.hasMisconfigurationsFindings} instead`
);
});
it(`Return hasMisconfigurationsFindings true when there are only findings in third party index`, async () => {
await deleteIndex(es, INDEX_ARRAY);
const mock3PIndex = 'logs-mock-3p-integration_latest_misconfigurations_cdr';
await addIndex(es, findingsMockData, mock3PIndex);
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set('kbn-xsrf', 'xxxx')
.expect(200);
expect(res.hasMisconfigurationsFindings).to.eql(
true,
`expected hasMisconfigurationsFindings to be true but got ${res.hasMisconfigurationsFindings} instead`
);
await deleteIndex(es, [mock3PIndex]);
});
it(`Return hasMisconfigurationsFindings false when there are no findings`, async () => {
await deleteIndex(es, INDEX_ARRAY);
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set('kbn-xsrf', 'xxxx')
.expect(200);
expect(res.hasMisconfigurationsFindings).to.eql(
false,
`expected hasMisconfigurationsFindings to be false but got ${res.hasMisconfigurationsFindings} instead`
);
});
it(`Return kspm status indexed when logs-cloud_security_posture.findings_latest-default contains new kspm documents`, async () => {
await createPackagePolicy(
supertest,