From 5728bfa1a7514eeb425e0894dab85ba63f48e8fa Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 26 Jun 2023 11:19:05 -0700 Subject: [PATCH] [CloudSecurity][Fleet] Add CloudFormation install method to CSPM (#159994) --- .../common/api/use_package_policy_list.ts | 36 +- .../aws_credentials_form.tsx | 313 +++++++++++++++ .../get_aws_credentials_form_options.tsx | 176 +++++++++ .../aws_credentials_form/hooks.ts | 205 ++++++++++ ...ials_form.tsx => eks_credentials_form.tsx} | 120 +++--- .../components/fleet_extensions/mocks.ts | 40 +- .../policy_template_form.test.tsx | 356 ++++++++++++++---- .../fleet_extensions/policy_template_form.tsx | 28 +- .../policy_template_selectors.tsx | 12 +- .../components/fleet_extensions/utils.test.ts | 2 +- .../components/fleet_extensions/utils.ts | 34 +- .../post_install_cloud_formation_modal.tsx | 97 +++++ .../single_page_layout/hooks/form.tsx | 16 +- .../single_page_layout/index.tsx | 9 + .../create_package_policy_page/types.ts | 3 +- .../cloud_formation_instructions.tsx | 73 +--- .../agent_enrollment_flyout/hooks.tsx | 4 +- .../components/cloud_formation_guide.tsx | 91 +++++ .../plugins/fleet/public/components/index.ts | 1 + x-pack/plugins/fleet/public/hooks/index.ts | 1 + .../hooks/use_create_cloud_formation_url.ts | 74 ++++ ...ion_template_url_from_agent_policy.test.ts | 68 ++++ ...ormation_template_url_from_agent_policy.ts | 38 ++ ...n_template_url_from_package_policy.test.ts | 61 +++ ...mation_template_url_from_package_policy.ts | 32 +- x-pack/plugins/fleet/public/services/index.ts | 1 + .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 29 files changed, 1643 insertions(+), 260 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/hooks.ts rename x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/{aws_credentials_form.tsx => eks_credentials_form.tsx} (72%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx create mode 100644 x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx create mode 100644 x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts create mode 100644 x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts create mode 100644 x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts create mode 100644 x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_package_policy_list.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_package_policy_list.ts index 6df792029084e..d9b9c08a9bea1 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_package_policy_list.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_package_policy_list.ts @@ -21,24 +21,32 @@ interface PackagePolicyListData { const PACKAGE_POLICY_LIST_QUERY_KEY = ['packagePolicyList']; -export const usePackagePolicyList = (packageInfoName: string) => { +export const usePackagePolicyList = (packageInfoName: string, { enabled = true }) => { const { http } = useKibana().services; - const query = useQuery(PACKAGE_POLICY_LIST_QUERY_KEY, async () => { - try { - const res = await http.get(packagePolicyRouteService.getListPath(), { - query: { - perPage: SO_SEARCH_LIMIT, - page: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfoName}`, - }, - }); + const query = useQuery( + PACKAGE_POLICY_LIST_QUERY_KEY, + async () => { + try { + const res = await http.get(packagePolicyRouteService.getListPath(), { + query: { + perPage: SO_SEARCH_LIMIT, + page: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfoName}`, + }, + }); - return res; - } catch (error: any) { - throw new Error(`Failed to fetch package policy list: ${error.message}`); + return res; + } catch (error: any) { + throw new Error(`Failed to fetch package policy list: ${error.message}`); + } + }, + { + enabled, + refetchOnMount: false, + refetchOnWindowFocus: false, } - }); + ); return query; }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx new file mode 100644 index 0000000000000..8110d1f92bbc8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx @@ -0,0 +1,313 @@ +/* + * 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 { + EuiFieldText, + EuiFieldPassword, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, + EuiSelect, + EuiCallOut, +} from '@elastic/eui'; +import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; +import { PackageInfo } from '@kbn/fleet-plugin/common'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { + getAwsCredentialsFormManualOptions, + AwsCredentialsType, + AwsOptions, + DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE, +} from './get_aws_credentials_form_options'; +import { RadioGroup } from '../csp_boxed_radio_group'; +import { + getCspmCloudFormationDefaultValue, + getPosturePolicy, + NewPackagePolicyPostureInput, +} from '../utils'; +import { SetupFormat, useAwsCredentialsForm } from './hooks'; + +interface AWSSetupInfoContentProps { + integrationLink: string; +} + +const AWSSetupInfoContent = ({ integrationLink }: AWSSetupInfoContentProps) => { + return ( + <> + + +

+ +

+
+ + + + + + ), + }} + /> + + + ); +}; + +const getSetupFormatOptions = (): Array<{ id: SetupFormat; label: string }> => [ + { + id: 'cloud_formation', + label: 'CloudFormation', + }, + { + id: 'manual', + label: i18n.translate('xpack.csp.awsIntegration.setupFormatOptions.manual', { + defaultMessage: 'Manual', + }), + }, +]; + +export const getDefaultAwsVarsGroup = (packageInfo: PackageInfo): AwsCredentialsType => { + const hasCloudFormationTemplate = !!getCspmCloudFormationDefaultValue(packageInfo); + if (hasCloudFormationTemplate) { + return 'cloud_formation'; + } + + return DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE; +}; + +interface Props { + newPolicy: NewPackagePolicy; + input: Extract; + updatePolicy(updatedPolicy: NewPackagePolicy): void; + packageInfo: PackageInfo; + onChange: any; + setIsValid: (isValid: boolean) => void; +} + +const CloudFormationSetup = ({ + hasCloudFormationTemplate, +}: { + hasCloudFormationTemplate: boolean; +}) => { + if (!hasCloudFormationTemplate) { + return ( + + + + ); + } + return ( + <> + +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
+
+ + + + ); +}; + +const CLOUD_FORMATION_EXTERNAL_DOC_URL = + 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-whatis-howdoesitwork.html'; + +const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ( + + {children} + +); + +const ReadDocumentation = ({ url }: { url: string }) => { + return ( + + + {i18n.translate('xpack.csp.awsIntegration.documentationLinkText', { + defaultMessage: 'documentation', + })} + + ), + }} + /> + + ); +}; + +export const AwsCredentialsForm = ({ + input, + newPolicy, + updatePolicy, + packageInfo, + onChange, + setIsValid, +}: Props) => { + const { + awsCredentialsType, + setupFormat, + group, + fields, + integrationLink, + hasCloudFormationTemplate, + onSetupFormatChange, + } = useAwsCredentialsForm({ + newPolicy, + input, + packageInfo, + onChange, + setIsValid, + updatePolicy, + }); + + return ( + <> + + + + + {setupFormat === 'cloud_formation' && ( + + )} + {setupFormat === 'manual' && ( + <> + { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'aws.credentials.type': { value: optionId }, + }) + ); + }} + /> + + {group.info} + + + + { + updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })); + }} + /> + + )} + + + ); +}; +const AwsCredentialTypeSelector = ({ + type, + onChange, +}: { + onChange(type: AwsCredentialsType): void; + type: AwsCredentialsType; +}) => ( + + { + onChange(optionElem.target.value as AwsCredentialsType); + }} + /> + +); + +const AwsInputVarFields = ({ + fields, + onChange, +}: { + fields: Array; + onChange: (key: string, value: string) => void; +}) => ( +
+ {fields.map((field) => ( + + <> + {field.type === 'password' && ( + onChange(field.id, event.target.value)} + /> + )} + {field.type === 'text' && ( + onChange(field.id, event.target.value)} + /> + )} + + + ))} +
+); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx new file mode 100644 index 0000000000000..4b245b9a992ac --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx @@ -0,0 +1,176 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; + +const AssumeRoleDescription = ( +
+ + + +
+); + +const DirectAccessKeysDescription = ( +
+ + + +
+); + +const TemporaryKeysDescription = ( +
+ + + +
+); + +const SharedCredentialsDescription = ( +
+ + + +
+); + +const AWS_FIELD_LABEL = { + access_key_id: i18n.translate('xpack.csp.awsIntegration.accessKeyIdLabel', { + defaultMessage: 'Access Key ID', + }), + secret_access_key: i18n.translate('xpack.csp.awsIntegration.secretAccessKeyLabel', { + defaultMessage: 'Secret Access Key', + }), +}; + +export type AwsCredentialsType = + | 'assume_role' + | 'direct_access_keys' + | 'temporary_keys' + | 'shared_credentials' + | 'cloud_formation'; + +export type AwsCredentialsFields = Record; + +export interface AwsOptionValue { + label: string; + info: React.ReactNode; + fields: AwsCredentialsFields; +} + +export const getInputVarsFields = (input: NewPackagePolicyInput, fields: AwsCredentialsFields) => + Object.entries(input.streams[0].vars || {}) + .filter(([id]) => id in fields) + .map(([id, inputVar]) => { + const field = fields[id]; + return { + id, + label: field.label, + type: field.type || 'text', + value: inputVar.value, + } as const; + }); + +export type AwsOptions = Record; + +export const getAwsCredentialsFormManualOptions = (): Array<{ + value: AwsCredentialsType; + text: string; +}> => { + return Object.entries(getAwsCredentialsFormOptions()).map(([key, value]) => ({ + value: key as AwsCredentialsType, + text: value.label, + })); +}; + +export const DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE = 'assume_role'; + +export const getAwsCredentialsFormOptions = (): AwsOptions => ({ + assume_role: { + label: i18n.translate('xpack.csp.awsIntegration.assumeRoleLabel', { + defaultMessage: 'Assume role', + }), + info: AssumeRoleDescription, + fields: { + role_arn: { + label: i18n.translate('xpack.csp.awsIntegration.roleArnLabel', { + defaultMessage: 'Role ARN', + }), + }, + }, + }, + direct_access_keys: { + label: i18n.translate('xpack.csp.awsIntegration.directAccessKeyLabel', { + defaultMessage: 'Direct access keys', + }), + info: DirectAccessKeysDescription, + fields: { + access_key_id: { label: AWS_FIELD_LABEL.access_key_id }, + secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' }, + }, + }, + temporary_keys: { + info: TemporaryKeysDescription, + label: i18n.translate('xpack.csp.awsIntegration.temporaryKeysLabel', { + defaultMessage: 'Temporary keys', + }), + fields: { + access_key_id: { label: AWS_FIELD_LABEL.access_key_id }, + secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' }, + session_token: { + label: i18n.translate('xpack.csp.awsIntegration.sessionTokenLabel', { + defaultMessage: 'Session Token', + }), + }, + }, + }, + shared_credentials: { + label: i18n.translate('xpack.csp.awsIntegration.sharedCredentialLabel', { + defaultMessage: 'Shared credentials', + }), + info: SharedCredentialsDescription, + fields: { + shared_credential_file: { + label: i18n.translate('xpack.csp.awsIntegration.sharedCredentialFileLabel', { + defaultMessage: 'Shared Credential File', + }), + }, + credential_profile_name: { + label: i18n.translate('xpack.csp.awsIntegration.credentialProfileNameLabel', { + defaultMessage: 'Credential Profile Name', + }), + }, + }, + }, + cloud_formation: { + label: 'CloudFormation', + info: [], + fields: {}, + }, +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/hooks.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/hooks.ts new file mode 100644 index 0000000000000..c689c99b52dfe --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/hooks.ts @@ -0,0 +1,205 @@ +/* + * 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 { useEffect, useRef } from 'react'; +import { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common'; +import { cspIntegrationDocsNavigation } from '../../../common/navigation/constants'; +import { + getCspmCloudFormationDefaultValue, + getPosturePolicy, + NewPackagePolicyPostureInput, +} from '../utils'; +import { + AwsCredentialsType, + DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE, + getAwsCredentialsFormOptions, + getInputVarsFields, +} from './get_aws_credentials_form_options'; +import { CLOUDBEAT_AWS } from '../../../../common/constants'; +/** + * Update CloudFormation template and stack name in the Agent Policy + * based on the selected policy template + */ + +export type SetupFormat = 'cloud_formation' | 'manual'; + +const getSetupFormatFromInput = ( + input: Extract, + hasCloudFormationTemplate: boolean +): SetupFormat => { + const credentialsType = getAwsCredentialsType(input); + // CloudFormation is the default setup format if the integration has a CloudFormation template + if (!credentialsType && hasCloudFormationTemplate) { + return 'cloud_formation'; + } + if (credentialsType !== 'cloud_formation') { + return 'manual'; + } + + return 'cloud_formation'; +}; + +const getAwsCredentialsType = ( + input: Extract +): AwsCredentialsType | undefined => input.streams[0].vars?.['aws.credentials.type'].value; + +export const useAwsCredentialsForm = ({ + newPolicy, + input, + packageInfo, + onChange, + setIsValid, + updatePolicy, +}: { + newPolicy: NewPackagePolicy; + input: Extract; + packageInfo: PackageInfo; + onChange: (opts: any) => void; + setIsValid: (isValid: boolean) => void; + updatePolicy: (updatedPolicy: NewPackagePolicy) => void; +}) => { + // We only have a value for 'aws.credentials.type' once the form has mounted. + // On initial render we don't have that value so we fallback to the default option. + const awsCredentialsType: AwsCredentialsType = + getAwsCredentialsType(input) || DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE; + + const options = getAwsCredentialsFormOptions(); + + const hasCloudFormationTemplate = !!getCspmCloudFormationDefaultValue(packageInfo); + + const setupFormat = getSetupFormatFromInput(input, hasCloudFormationTemplate); + + const group = options[awsCredentialsType]; + const fields = getInputVarsFields(input, group.fields); + const fieldsSnapshot = useRef({}); + const lastManualCredentialsType = useRef(undefined); + + useEffect(() => { + const isInvalid = setupFormat === 'cloud_formation' && !hasCloudFormationTemplate; + + setIsValid(!isInvalid); + + onChange({ + isValid: !isInvalid, + updatedPolicy: newPolicy, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setupFormat, input.type]); + + const integrationLink = cspIntegrationDocsNavigation.cspm.getStartedPath; + + useCloudFormationTemplate({ + packageInfo, + newPolicy, + updatePolicy, + setupFormat, + }); + + const onSetupFormatChange = (newSetupFormat: SetupFormat) => { + if (newSetupFormat === 'cloud_formation') { + // We need to store the current manual fields to restore them later + fieldsSnapshot.current = Object.fromEntries( + fields.map((field) => [field.id, { value: field.value }]) + ); + // We need to store the last manual credentials type to restore it later + lastManualCredentialsType.current = getAwsCredentialsType(input); + + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'aws.credentials.type': { + value: 'cloud_formation', + type: 'text', + }, + // Clearing fields from previous setup format to prevent exposing credentials + // when switching from manual to cloud formation + ...Object.fromEntries(fields.map((field) => [field.id, { value: undefined }])), + }) + ); + } else { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'aws.credentials.type': { + // Restoring last manual credentials type or defaulting to the first option + value: lastManualCredentialsType.current || DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE, + type: 'text', + }, + // Restoring fields from manual setup format if any + ...fieldsSnapshot.current, + }) + ); + } + }; + + return { + awsCredentialsType, + setupFormat, + group, + fields, + integrationLink, + hasCloudFormationTemplate, + onSetupFormatChange, + }; +}; + +const getAwsCloudFormationTemplate = (newPolicy: NewPackagePolicy) => { + const template: string | undefined = newPolicy?.inputs?.find((i) => i.type === CLOUDBEAT_AWS) + ?.config?.cloud_formation_template_url?.value; + + return template || undefined; +}; + +const updateCloudFormationPolicyTemplate = ( + newPolicy: NewPackagePolicy, + updatePolicy: (policy: NewPackagePolicy) => void, + templateUrl: string | undefined +) => { + updatePolicy?.({ + ...newPolicy, + inputs: newPolicy.inputs.map((input) => { + if (input.type === CLOUDBEAT_AWS) { + return { + ...input, + config: { cloud_formation_template_url: { value: templateUrl } }, + }; + } + return input; + }), + }); +}; + +const useCloudFormationTemplate = ({ + packageInfo, + newPolicy, + updatePolicy, + setupFormat, +}: { + packageInfo: PackageInfo; + newPolicy: NewPackagePolicy; + updatePolicy: (policy: NewPackagePolicy) => void; + setupFormat: SetupFormat; +}) => { + useEffect(() => { + const policyInputCloudFormationTemplate = getAwsCloudFormationTemplate(newPolicy); + + if (setupFormat === 'manual') { + if (!!policyInputCloudFormationTemplate) { + updateCloudFormationPolicyTemplate(newPolicy, updatePolicy, undefined); + } + return; + } + const templateUrl = getCspmCloudFormationDefaultValue(packageInfo); + + // If the template is not available, do not update the policy + if (templateUrl === '') return; + + // If the template is already set, do not update the policy + if (policyInputCloudFormationTemplate === templateUrl) return; + + updateCloudFormationPolicyTemplate(newPolicy, updatePolicy, templateUrl); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newPolicy?.vars?.cloud_formation_template_url, newPolicy, packageInfo, setupFormat]); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx similarity index 72% rename from x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form.tsx rename to x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx index a0932d2efd781..b14cb1cd9cdc7 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx @@ -18,60 +18,56 @@ import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { CSPM_POLICY_TEMPLATE } from '../../../common/constants'; -import { PosturePolicyTemplate } from '../../../common/types'; import { RadioGroup } from './csp_boxed_radio_group'; import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils'; -import { cspIntegrationDocsNavigation } from '../../common/navigation/constants'; -interface AWSSetupInfoContentProps { - policyTemplate: PosturePolicyTemplate | undefined; -} - -const AWSSetupInfoContent = ({ policyTemplate }: AWSSetupInfoContentProps) => { - const { cspm, kspm } = cspIntegrationDocsNavigation; - const integrationLink = - !policyTemplate || policyTemplate === CSPM_POLICY_TEMPLATE - ? cspm.getStartedPath - : kspm.getStartedPath; - - return ( - <> - - -

- -

-
- - +const AWSSetupInfoContent = () => ( + <> + + +

- - - ), - }} + id="xpack.csp.eksIntegration.setupInfoContentTitle" + defaultMessage="Setup Access" /> - - - ); -}; +

+
+ + + + + +); + +const DocsLink = ( + + + documentation + + ), + }} + /> + +); const AssumeRoleDescription = (
@@ -95,7 +91,7 @@ const TemporaryKeysDescription = (
@@ -117,10 +113,10 @@ const SharedCredentialsDescription = ( ); const AWS_FIELD_LABEL = { - access_key_id: i18n.translate('xpack.csp.awsIntegration.accessKeyIdLabel', { + access_key_id: i18n.translate('xpack.csp.eksIntegration.accessKeyIdLabel', { defaultMessage: 'Access Key ID', }), - secret_access_key: i18n.translate('xpack.csp.awsIntegration.secretAccessKeyLabel', { + secret_access_key: i18n.translate('xpack.csp.eksIntegration.secretAccessKeyLabel', { defaultMessage: 'Secret Access Key', }), }; @@ -136,20 +132,20 @@ type AwsOptions = Record< const options: AwsOptions = { assume_role: { - label: i18n.translate('xpack.csp.awsIntegration.assumeRoleLabel', { + label: i18n.translate('xpack.csp.eksIntegration.assumeRoleLabel', { defaultMessage: 'Assume role', }), info: AssumeRoleDescription, fields: { role_arn: { - label: i18n.translate('xpack.csp.awsIntegration.roleArnLabel', { + label: i18n.translate('xpack.csp.eksIntegration.roleArnLabel', { defaultMessage: 'Role ARN', }), }, }, }, direct_access_keys: { - label: i18n.translate('xpack.csp.awsIntegration.directAccessKeyLabel', { + label: i18n.translate('xpack.csp.eksIntegration.directAccessKeyLabel', { defaultMessage: 'Direct access keys', }), info: DirectAccessKeysDescription, @@ -160,32 +156,32 @@ const options: AwsOptions = { }, temporary_keys: { info: TemporaryKeysDescription, - label: i18n.translate('xpack.csp.awsIntegration.temporaryKeysLabel', { + label: i18n.translate('xpack.csp.eksIntegration.temporaryKeysLabel', { defaultMessage: 'Temporary keys', }), fields: { access_key_id: { label: AWS_FIELD_LABEL.access_key_id }, secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' }, session_token: { - label: i18n.translate('xpack.csp.awsIntegration.sessionTokenLabel', { + label: i18n.translate('xpack.csp.eksIntegration.sessionTokenLabel', { defaultMessage: 'Session Token', }), }, }, }, shared_credentials: { - label: i18n.translate('xpack.csp.awsIntegration.sharedCredentialLabel', { + label: i18n.translate('xpack.csp.eksIntegration.sharedCredentialLabel', { defaultMessage: 'Shared credentials', }), info: SharedCredentialsDescription, fields: { shared_credential_file: { - label: i18n.translate('xpack.csp.awsIntegration.sharedCredentialFileLabel', { + label: i18n.translate('xpack.csp.eksIntegration.sharedCredentialFileLabel', { defaultMessage: 'Shared Credential File', }), }, credential_profile_name: { - label: i18n.translate('xpack.csp.awsIntegration.credentialProfileNameLabel', { + label: i18n.translate('xpack.csp.eksIntegration.credentialProfileNameLabel', { defaultMessage: 'Credential Profile Name', }), }, @@ -194,7 +190,7 @@ const options: AwsOptions = { }; export type AwsCredentialsType = keyof typeof options; -export const DEFAULT_AWS_VARS_GROUP: AwsCredentialsType = 'assume_role'; +export const DEFAULT_EKS_VARS_GROUP: AwsCredentialsType = 'assume_role'; const AWS_CREDENTIALS_OPTIONS = Object.keys(options).map((value) => ({ id: value as AwsCredentialsType, label: options[value as keyof typeof options].label, @@ -225,7 +221,7 @@ const getInputVarsFields = ( const getAwsCredentialsType = (input: Props['input']): AwsCredentialsType | undefined => input.streams[0].vars?.['aws.credentials.type'].value; -export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => { +export const EksCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => { // We only have a value for 'aws.credentials.type' once the form has mounted. // On initial render we don't have that value so we default to the first option. const awsCredentialsType = getAwsCredentialsType(input) || AWS_CREDENTIALS_OPTIONS[0].id; @@ -234,7 +230,7 @@ export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => return ( <> - + /> {group.info} - + + {DocsLink} + diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts index 4bdfa4bc542dd..151952f08a23c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts @@ -50,6 +50,34 @@ export const getMockPackageInfoVulnMgmtAWS = () => { } as PackageInfo; }; +export const getMockPackageInfoCspmAWS = () => { + return { + name: 'cspm', + policy_templates: [ + { + title: '', + description: '', + name: 'cspm', + inputs: [ + { + type: CLOUDBEAT_AWS, + title: '', + description: '', + vars: [ + { + type: 'text', + name: 'cloud_formation_template', + default: 's3_url', + show_user: false, + }, + ], + }, + ], + }, + ], + } as PackageInfo; +}; + const getPolicyMock = ( type: PostureInput, posture: string, @@ -58,6 +86,16 @@ const getPolicyMock = ( const mockPackagePolicy = createNewPackagePolicyMock(); const awsVarsMock = { + access_key_id: { type: 'text' }, + secret_access_key: { type: 'text' }, + session_token: { type: 'text' }, + shared_credential_file: { type: 'text' }, + credential_profile_name: { type: 'text' }, + role_arn: { type: 'text' }, + 'aws.credentials.type': { value: 'cloud_formation', type: 'text' }, + }; + + const eksVarsMock = { access_key_id: { type: 'text' }, secret_access_key: { type: 'text' }, session_token: { type: 'text' }, @@ -95,7 +133,7 @@ const getPolicyMock = ( type: CLOUDBEAT_EKS, policy_template: 'kspm', enabled: type === CLOUDBEAT_EKS, - streams: [{ enabled: type === CLOUDBEAT_EKS, data_stream: dataStream, vars: awsVarsMock }], + streams: [{ enabled: type === CLOUDBEAT_EKS, data_stream: dataStream, vars: eksVarsMock }], }, { type: CLOUDBEAT_AWS, diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index 84c299ade66db..7bb2e882fd387 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -9,6 +9,7 @@ import { render } from '@testing-library/react'; import { CspPolicyTemplateForm } from './policy_template_form'; import { TestProvider } from '../../test/test_provider'; import { + getMockPackageInfoCspmAWS, getMockPackageInfoVulnMgmtAWS, getMockPolicyAWS, getMockPolicyEKS, @@ -281,7 +282,7 @@ describe('', () => { }); // 1st call happens on mount and selects the default policy template enabled input - expect(onChange).toHaveBeenNthCalledWith(1, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyK8s(), @@ -290,7 +291,7 @@ describe('', () => { }); // 2nd call happens on mount and increments kspm template enabled input - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyK8s(), @@ -302,7 +303,7 @@ describe('', () => { }, }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyK8s(), @@ -369,7 +370,7 @@ describe('', () => { }); // 1st call happens on mount and selects the default policy template enabled input - expect(onChange).toHaveBeenNthCalledWith(1, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyVulnMgmtAWS(), @@ -378,7 +379,7 @@ describe('', () => { }); // 2nd call happens on mount and increments vuln_mgmt template enabled input - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyVulnMgmtAWS(), @@ -391,7 +392,7 @@ describe('', () => { }); // 3rd call happens on mount and increments vuln_mgmt template enabled input - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyVulnMgmtAWS(), @@ -416,6 +417,7 @@ describe('', () => { (useParams as jest.Mock).mockReturnValue({ integration: 'cspm', }); + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', @@ -440,12 +442,31 @@ describe('', () => { render( ); - onChange({ + // 1st call happens on mount and selects the CloudFormation template + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: { + ...getMockPolicyAWS(), + name: 'cloud_security_posture-1', + inputs: policy.inputs.map((input) => { + if (input.type === CLOUDBEAT_AWS) { + return { + ...input, + enabled: true, + }; + } + return input; + }), + }, + }); + + // 2nd call happens on mount and increments cspm template enabled input + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), @@ -453,21 +474,30 @@ describe('', () => { ...input, enabled: input.policy_template === 'cspm', })), - name: 'cspm-2', + name: 'cspm-1', }, }); - // 1st call happens on mount and selects the default policy template enabled input - expect(onChange).toHaveBeenNthCalledWith(1, { + // // 3rd call happens on mount and increments cspm template enabled input + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), + inputs: policy.inputs.map((input) => { + if (input.type === CLOUDBEAT_AWS) { + return { + ...input, + enabled: true, + config: { cloud_formation_template_url: { value: 's3_url' } }, + }; + } + return input; + }), name: 'cloud_security_posture-1', }, }); - // 2nd call happens on mount and increments cspm template enabled input - expect(onChange).toHaveBeenNthCalledWith(2, { + onChange({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), @@ -475,12 +505,11 @@ describe('', () => { ...input, enabled: input.policy_template === 'cspm', })), - name: 'cspm-1', + name: 'cspm-2', }, }); - // 3rd call happens on mount and increments cspm template enabled input - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), @@ -493,73 +522,69 @@ describe('', () => { }); }); - /** - * AWS Credentials input fields tests for KSPM/CSPM integrations - */ - const awsInputs = { - [CLOUDBEAT_EKS]: getMockPolicyEKS, - [CLOUDBEAT_AWS]: getMockPolicyAWS, - }; - - for (const [inputKey, getPolicy] of Object.entries(awsInputs) as Array< - [keyof typeof awsInputs, typeof awsInputs[keyof typeof awsInputs]] - >) { - it(`renders ${inputKey} Assume Role fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + describe('EKS Credentials input fields', () => { + it(`renders ${CLOUDBEAT_EKS} Assume Role fields`, () => { + let policy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'assume_role' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText } = render(); - const option = getByLabelText('Assume role'); + const option = getByLabelText('Assume role'); expect(option).toBeChecked(); + expect(getByLabelText('Role ARN')).toBeInTheDocument(); }); - it(`updates ${inputKey} Assume Role fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`updates ${CLOUDBEAT_EKS} Assume Role fields`, () => { + let policy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'assume_role' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText } = render(); userEvent.type(getByLabelText('Role ARN'), 'a'); - policy = getPosturePolicy(policy, inputKey, { role_arn: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { role_arn: { value: 'a' } }); // Ignore 1st call triggered on mount to ensure initial state is valid - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); }); - it(`renders ${inputKey} Direct Access Keys fields`, () => { - let policy: NewPackagePolicy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`renders ${CLOUDBEAT_EKS} Direct Access Keys fields`, () => { + let policy: NewPackagePolicy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'direct_access_keys' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText } = render(); - const option = getByLabelText('Direct access keys'); + const option = getByLabelText('Direct access keys'); expect(option).toBeChecked(); + expect(getByLabelText('Access Key ID')).toBeInTheDocument(); expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); }); - it(`updates ${inputKey} Direct Access Keys fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`updates ${CLOUDBEAT_EKS} Direct Access Keys fields`, () => { + let policy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'direct_access_keys' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText, rerender } = render(); userEvent.type(getByLabelText('Access Key ID'), 'a'); - policy = getPosturePolicy(policy, inputKey, { access_key_id: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } }); // Ignore 1st call triggered on mount to ensure initial state is valid - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -567,41 +592,43 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Secret Access Key'), 'b'); - policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); }); - it(`renders ${inputKey} Temporary Keys fields`, () => { - let policy: NewPackagePolicy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`renders ${CLOUDBEAT_EKS} Temporary Keys fields`, () => { + let policy: NewPackagePolicy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'temporary_keys' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText } = render(); - const option = getByLabelText('Temporary keys'); + const option = getByLabelText('Temporary keys'); expect(option).toBeChecked(); + expect(getByLabelText('Access Key ID')).toBeInTheDocument(); expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); expect(getByLabelText('Session Token')).toBeInTheDocument(); }); - it(`updates ${inputKey} Temporary Keys fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`updates ${CLOUDBEAT_EKS} Temporary Keys fields`, () => { + let policy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'temporary_keys' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText, rerender } = render(); userEvent.type(getByLabelText('Access Key ID'), 'a'); - policy = getPosturePolicy(policy, inputKey, { access_key_id: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } }); - // Ignore 1st call triggered on mount to ensure initial state is valid - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -609,9 +636,9 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Secret Access Key'), 'b'); - policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -619,42 +646,223 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Session Token'), 'a'); - policy = getPosturePolicy(policy, inputKey, { session_token: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { session_token: { value: 'a' } }); - expect(onChange).toHaveBeenNthCalledWith(4, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); }); - it(`renders ${inputKey} Shared Credentials fields`, () => { - let policy: NewPackagePolicy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`renders ${CLOUDBEAT_EKS} Shared Credentials fields`, () => { + let policy: NewPackagePolicy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'shared_credentials' }, }); const { getByLabelText } = render(); - const option = getByLabelText('Shared credentials'); + const option = getByLabelText('Shared credentials'); expect(option).toBeChecked(); + expect(getByLabelText('Shared Credential File')).toBeInTheDocument(); expect(getByLabelText('Credential Profile Name')).toBeInTheDocument(); }); - it(`updates ${inputKey} Shared Credentials fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`updates ${CLOUDBEAT_EKS} Shared Credentials fields`, () => { + let policy = getMockPolicyEKS(); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { 'aws.credentials.type': { value: 'shared_credentials' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText, rerender } = render(); userEvent.type(getByLabelText('Shared Credential File'), 'a'); - policy = getPosturePolicy(policy, inputKey, { + + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { shared_credential_file: { value: 'a' }, }); + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + + rerender(); + + userEvent.type(getByLabelText('Credential Profile Name'), 'b'); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { + credential_profile_name: { value: 'b' }, + }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + }); + + describe('AWS Credentials input fields', () => { + it(`renders ${CLOUDBEAT_AWS} Assume Role fields`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'assume_role' }, + 'aws.setup.format': { value: 'manual' }, + }); + + const { getByLabelText, getByRole } = render(); + + expect(getByRole('option', { name: 'Assume role', selected: true })).toBeInTheDocument(); + + expect(getByLabelText('Role ARN')).toBeInTheDocument(); + }); + + it(`updates ${CLOUDBEAT_AWS} Assume Role fields`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'assume_role' }, + 'aws.setup.format': { value: 'manual' }, + }); + const { getByLabelText } = render(); + + userEvent.type(getByLabelText('Role ARN'), 'a'); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { role_arn: { value: 'a' } }); + // Ignore 1st call triggered on mount to ensure initial state is valid - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + + it(`renders ${CLOUDBEAT_AWS} Direct Access Keys fields`, () => { + let policy: NewPackagePolicy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'direct_access_keys' }, + 'aws.setup.format': { value: 'manual' }, + }); + + const { getByLabelText, getByRole } = render(); + + expect( + getByRole('option', { name: 'Direct access keys', selected: true }) + ).toBeInTheDocument(); + + expect(getByLabelText('Access Key ID')).toBeInTheDocument(); + expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); + }); + + it(`updates ${CLOUDBEAT_AWS} Direct Access Keys fields`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'direct_access_keys' }, + 'aws.setup.format': { value: 'manual' }, + }); + const { getByLabelText, rerender } = render(); + + userEvent.type(getByLabelText('Access Key ID'), 'a'); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } }); + + // Ignore 1st call triggered on mount to ensure initial state is valid + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + + rerender(); + + userEvent.type(getByLabelText('Secret Access Key'), 'b'); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + + it(`renders ${CLOUDBEAT_AWS} Temporary Keys fields`, () => { + let policy: NewPackagePolicy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'temporary_keys' }, + 'aws.setup.format': { value: 'manual' }, + }); + + const { getByLabelText, getByRole } = render(); + expect(getByRole('option', { name: 'Temporary keys', selected: true })).toBeInTheDocument(); + + expect(getByLabelText('Access Key ID')).toBeInTheDocument(); + expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); + expect(getByLabelText('Session Token')).toBeInTheDocument(); + }); + + it(`updates ${CLOUDBEAT_AWS} Temporary Keys fields`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'temporary_keys' }, + 'aws.setup.format': { value: 'manual' }, + }); + const { getByLabelText, rerender } = render(); + + userEvent.type(getByLabelText('Access Key ID'), 'a'); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + + rerender(); + + userEvent.type(getByLabelText('Secret Access Key'), 'b'); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + + rerender(); + + userEvent.type(getByLabelText('Session Token'), 'a'); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { session_token: { value: 'a' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + + it(`renders ${CLOUDBEAT_AWS} Shared Credentials fields`, () => { + let policy: NewPackagePolicy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'shared_credentials' }, + }); + + const { getByLabelText, getByRole } = render(); + + expect( + getByRole('option', { name: 'Shared credentials', selected: true }) + ).toBeInTheDocument(); + + expect(getByLabelText('Shared Credential File')).toBeInTheDocument(); + expect(getByLabelText('Credential Profile Name')).toBeInTheDocument(); + }); + + it(`updates ${CLOUDBEAT_AWS} Shared Credentials fields`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'shared_credentials' }, + 'aws.setup.format': { value: 'manual' }, + }); + const { getByLabelText, rerender } = render(); + + userEvent.type(getByLabelText('Shared Credential File'), 'a'); + + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + shared_credential_file: { value: 'a' }, + }); + + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -662,16 +870,16 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Credential Profile Name'), 'b'); - policy = getPosturePolicy(policy, inputKey, { + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { credential_profile_name: { value: 'b' }, }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); }); - } + }); describe('Vuln Mgmt', () => { it('Update Agent Policy CloudFormation template from vars', () => { @@ -693,7 +901,7 @@ describe('', () => { }), }; - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: expectedUpdatedPolicy, }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index d4610156afa8d..a5becf6644b24 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -94,12 +94,14 @@ export const CspPolicyTemplateForm = memo onChange({ isValid: true, updatedPolicy }), - [onChange] + (updatedPolicy: NewPackagePolicy) => onChange({ isValid, updatedPolicy }), + [onChange, isValid] ); /** * - Updates policy inputs by user selection @@ -107,11 +109,11 @@ export const CspPolicyTemplateForm = memo { - const inputVars = getPostureInputHiddenVars(inputType); + const inputVars = getPostureInputHiddenVars(inputType, packageInfo); const policy = getPosturePolicy(newPolicy, inputType, inputVars); updatePolicy(policy); }, - [newPolicy, updatePolicy] + [newPolicy, updatePolicy, packageInfo] ); // search for non null fields of the validation?.vars object @@ -120,6 +122,7 @@ export const CspPolicyTemplateForm = memo 0); + const [canFetchIntegration, setCanFetchIntegration] = useState(true); // delaying component rendering due to a race condition issue from Fleet // TODO: remove this workaround when the following issue is resolved: @@ -133,7 +136,9 @@ export const CspPolicyTemplateForm = memo setIsLoading(false), 200); }, [validationResultsNonNullFields]); - const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name); + const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name, { + enabled: canFetchIntegration, + }); useEffect(() => { if (isEditPage) return; @@ -161,6 +166,7 @@ export const CspPolicyTemplateForm = memo updatePolicy({ ...newPolicy, [field]: value })} /> {/* Defines the vars of the enabled input of the active policy template */} - + ); @@ -265,6 +278,7 @@ const usePolicyTemplateInitialName = ({ newPolicy, packagePolicyList, updatePolicy, + setCanFetchIntegration, }: { isEditPage: boolean; isLoading: boolean; @@ -272,6 +286,7 @@ const usePolicyTemplateInitialName = ({ newPolicy: NewPackagePolicy; packagePolicyList: PackagePolicy[] | undefined; updatePolicy: (policy: NewPackagePolicy) => void; + setCanFetchIntegration: (canFetch: boolean) => void; }) => { useEffect(() => { if (!integration) return; @@ -292,6 +307,7 @@ const usePolicyTemplateInitialName = ({ ...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]); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx index 814e5251238a2..f234ce0d5e15f 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx @@ -7,7 +7,8 @@ import React from 'react'; import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { NewPackagePolicy } from '@kbn/fleet-plugin/common'; +import type { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common'; +import { PackagePolicyReplaceDefineStepExtensionComponentProps } from '@kbn/fleet-plugin/public/types'; import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE, @@ -17,7 +18,8 @@ import { import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types'; import { getPolicyTemplateInputOptions, type NewPackagePolicyPostureInput } from './utils'; import { RadioGroup } from './csp_boxed_radio_group'; -import { AwsCredentialsForm } from './aws_credentials_form'; +import { AwsCredentialsForm } from './aws_credentials_form/aws_credentials_form'; +import { EksCredentialsForm } from './eks_credentials_form'; interface PolicyTemplateSelectorProps { selectedTemplate: CloudSecurityPolicyTemplate; @@ -66,13 +68,17 @@ interface PolicyTemplateVarsFormProps { newPolicy: NewPackagePolicy; input: NewPackagePolicyPostureInput; updatePolicy(updatedPolicy: NewPackagePolicy): void; + packageInfo: PackageInfo; + onChange: PackagePolicyReplaceDefineStepExtensionComponentProps['onChange']; + setIsValid: (isValid: boolean) => void; } export const PolicyTemplateVarsForm = ({ input, ...props }: PolicyTemplateVarsFormProps) => { switch (input.type) { case 'cloudbeat/cis_aws': - case 'cloudbeat/cis_eks': return ; + case 'cloudbeat/cis_eks': + return ; default: return null; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts index 3022b4ae2f8d4..323ab6aa7ef05 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts @@ -15,7 +15,7 @@ describe('getPosturePolicy', () => { ['cloudbeat/cis_k8s', getMockPolicyK8s, null], ] as const) { it(`updates package policy with hidden vars for ${name}`, () => { - const inputVars = getPostureInputHiddenVars(name); + const inputVars = getPostureInputHiddenVars(name, {} as any); const policy = getPosturePolicy(getPolicy(), name, inputVars); const enabledInputs = policy.inputs.filter( diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts index 32bec0489c9fa..6e8aecf5daf1b 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts @@ -26,9 +26,10 @@ import { KSPM_POLICY_TEMPLATE, VULN_MGMT_POLICY_TEMPLATE, } from '../../../common/constants'; -import { DEFAULT_AWS_VARS_GROUP } from './aws_credentials_form'; +import { getDefaultAwsVarsGroup } from './aws_credentials_form/aws_credentials_form'; import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types'; import { cloudPostureIntegrations } from '../../common/constants'; +import { DEFAULT_EKS_VARS_GROUP } from './eks_credentials_form'; // Posture policies only support the default namespace export const POSTURE_NAMESPACE = 'default'; @@ -101,7 +102,10 @@ const getPostureInput = ( ...(isInputEnabled && stream.vars && inputVars && { - vars: merge({}, stream.vars, inputVars), + vars: { + ...stream.vars, + ...inputVars, + }, }), })), }; @@ -160,14 +164,36 @@ export const getVulnMgmtCloudFormationDefaultValue = (packageInfo: PackageInfo): return cloudFormationTemplate; }; +export const getCspmCloudFormationDefaultValue = (packageInfo: PackageInfo): string => { + if (!packageInfo.policy_templates) return ''; + + const policyTemplate = packageInfo.policy_templates.find((p) => p.name === CSPM_POLICY_TEMPLATE); + if (!policyTemplate) return ''; + + const policyTemplateInputs = hasPolicyTemplateInputs(policyTemplate) && policyTemplate.inputs; + + if (!policyTemplateInputs) return ''; + + const cloudFormationTemplate = policyTemplateInputs.reduce((acc, input): string => { + if (!input.vars) return acc; + const template = input.vars.find((v) => v.name === 'cloud_formation_template')?.default; + return template ? String(template) : acc; + }, ''); + + return cloudFormationTemplate; +}; + /** * Input vars that are hidden from the user */ -export const getPostureInputHiddenVars = (inputType: PostureInput) => { +export const getPostureInputHiddenVars = (inputType: PostureInput, packageInfo: PackageInfo) => { switch (inputType) { case 'cloudbeat/cis_aws': + return { + 'aws.credentials.type': { value: getDefaultAwsVarsGroup(packageInfo), type: 'text' }, + }; case 'cloudbeat/cis_eks': - return { 'aws.credentials.type': { value: DEFAULT_AWS_VARS_GROUP } }; + return { 'aws.credentials.type': { value: DEFAULT_EKS_VARS_GROUP, type: 'text' } }; default: return undefined; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx new file mode 100644 index 0000000000000..33a706f8ca73b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx @@ -0,0 +1,97 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useQuery } from '@tanstack/react-query'; + +import type { AgentPolicy, PackagePolicy } from '../../../../../types'; +import { sendGetEnrollmentAPIKeys, useCreateCloudFormationUrl } from '../../../../../hooks'; +import { getCloudFormationTemplateUrlFromPackagePolicy } from '../../../../../services'; +import { CloudFormationGuide } from '../../../../../components'; + +export const PostInstallCloudFormationModal: React.FunctionComponent<{ + onConfirm: () => void; + onCancel: () => void; + agentPolicy: AgentPolicy; + packagePolicy: PackagePolicy; +}> = ({ onConfirm, onCancel, agentPolicy, packagePolicy }) => { + const { data: apyKeysData } = useQuery(['cloudFormationApiKeys'], () => + sendGetEnrollmentAPIKeys({ + page: 1, + perPage: 1, + kuery: `policy_id:${agentPolicy.id}`, + }) + ); + + const cloudFormationTemplateUrl = + getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy) || ''; + + const { cloudFormationUrl, error, isError, isLoading } = useCreateCloudFormationUrl({ + cloudFormationTemplateUrl, + enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, + }); + + return ( + + + + + + + + + + {error && isError && ( + <> + + + + )} + + + + + + + { + window.open(cloudFormationUrl); + onConfirm(); + }} + fill + color="primary" + isLoading={isLoading} + isDisabled={isError} + > + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 83b476b774913..52d02d93c8097 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -39,6 +39,7 @@ import type { PackagePolicyFormState } from '../../types'; import { SelectedPolicyTab } from '../../components'; import { useOnSaveNavigate } from '../../hooks'; import { prepareInputPackagePolicyDataset } from '../../services/prepare_input_pkg_policy_dataset'; +import { getCloudFormationTemplateUrlFromPackagePolicy } from '../../../../../services'; async function createAgentPolicy({ packagePolicy, @@ -298,11 +299,24 @@ export function useOnSubmit({ policy_id: createdPolicy?.id ?? packagePolicy.policy_id, force, }); - setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS'); + + const hasCloudFormation = data?.item + ? getCloudFormationTemplateUrlFromPackagePolicy(data.item) + : false; + + if (hasCloudFormation) { + setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_CLOUD_FORMATION'); + } else { + setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS'); + } if (!error) { setSavedPackagePolicy(data!.item); const hasAgentsAssigned = agentCount && agentPolicy; + if (!hasAgentsAssigned && hasCloudFormation) { + setFormState('SUBMITTED_CLOUD_FORMATION'); + return; + } if (!hasAgentsAssigned) { setFormState('SUBMITTED_NO_AGENTS'); return; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index fd62addd08391..c216135a8ffd3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -58,6 +58,7 @@ import { import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components'; import { useDevToolsRequest, useOnSubmit } from './hooks'; +import { PostInstallCloudFormationModal } from './components/post_install_cloud_formation_modal'; const StepsWithLessPadding = styled(EuiSteps)` .euiStep__content { @@ -412,6 +413,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} + {formState === 'SUBMITTED_CLOUD_FORMATION' && agentPolicy && savedPackagePolicy && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} {packageInfo && ( { - const cloudFormationUrl = templateURL - .replace('FLEET_ENROLLMENT_TOKEN', enrollmentToken) - .replace('FLEET_URL', fleetUrl) - .replace('KIBANA_VERSION', kibanaVersion); - - return new URL(cloudFormationUrl).toString(); -}; - export const CloudFormationInstructions: React.FunctionComponent = ({ enrollmentAPIKey, cloudFormationTemplateUrl, }) => { - const { data, isLoading } = useGetSettings(); - - const kibanaVersion = useKibanaVersion(); - - // Default fleet server host - const fleetServerHost = data?.item.fleet_server_hosts?.[0]; - - if (!isLoading && !fleetServerHost) { - return ( - <> - - - - ); - } + const { isLoading, cloudFormationUrl, error, isError } = useCreateCloudFormationUrl({ + enrollmentAPIKey, + cloudFormationTemplateUrl, + }); - if (!enrollmentAPIKey) { + if (error && isError) { return ( <> - + ); } - const cloudFormationUrl = createCloudFormationUrl( - cloudFormationTemplateUrl, - enrollmentAPIKey, - fleetServerHost || '', - kibanaVersion - ); - return ( = ({ } )} > - - - + ( + + {children} + +); + +export const CloudFormationGuide = () => { + return ( + +

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

+ +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
  7. + + + + ), + }} + /> +
  8. +
  9. + +
  10. +
+
+
+ ); +}; diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 06805679892f2..8335f9fcfc61f 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -29,3 +29,4 @@ export { DevtoolsRequestFlyoutButton } from './devtools_request_flyout'; export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge'; export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour'; export { UninstallCommandFlyout } from './uninstall_command_flyout'; +export { CloudFormationGuide } from './cloud_formation_guide'; diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index a9fb6ef7758c7..0692ce961379e 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -32,3 +32,4 @@ export * from './use_is_guided_onboarding_active'; export * from './use_fleet_server_hosts_for_policy'; export * from './use_fleet_server_standalone'; export * from './use_locator'; +export * from './use_create_cloud_formation_url'; diff --git a/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts b/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts new file mode 100644 index 0000000000000..cc76b68b6edb4 --- /dev/null +++ b/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts @@ -0,0 +1,74 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { useKibanaVersion } from './use_kibana_version'; +import { useGetSettings } from './use_request'; + +export const useCreateCloudFormationUrl = ({ + enrollmentAPIKey, + cloudFormationTemplateUrl, +}: { + enrollmentAPIKey: string | undefined; + cloudFormationTemplateUrl: string; +}) => { + const { data, isLoading } = useGetSettings(); + + const kibanaVersion = useKibanaVersion(); + + let isError = false; + let error: string | undefined; + + // Default fleet server host + const fleetServerHost = data?.item.fleet_server_hosts?.[0]; + + if (!fleetServerHost && !isLoading) { + isError = true; + error = i18n.translate('xpack.fleet.agentEnrollment.cloudFormation.noFleetServerHost', { + defaultMessage: 'No Fleet Server host found', + }); + } + + if (!enrollmentAPIKey && !isLoading) { + isError = true; + error = i18n.translate('xpack.fleet.agentEnrollment.cloudFormation.noApiKey', { + defaultMessage: 'No enrollment token found', + }); + } + + const cloudFormationUrl = + enrollmentAPIKey && fleetServerHost && cloudFormationTemplateUrl + ? createCloudFormationUrl( + cloudFormationTemplateUrl, + enrollmentAPIKey, + fleetServerHost, + kibanaVersion + ) + : undefined; + + return { + isLoading, + cloudFormationUrl, + isError, + error, + }; +}; + +const createCloudFormationUrl = ( + templateURL: string, + enrollmentToken: string, + fleetUrl: string, + kibanaVersion: string +) => { + const cloudFormationUrl = templateURL + .replace('FLEET_ENROLLMENT_TOKEN', enrollmentToken) + .replace('FLEET_URL', fleetUrl) + .replace('KIBANA_VERSION', kibanaVersion); + + return new URL(cloudFormationUrl).toString(); +}; diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts new file mode 100644 index 0000000000000..6b4214044f2a0 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy'; + +describe('getCloudFormationTemplateUrlFromAgentPolicy', () => { + it('should return undefined when selectedPolicy is undefined', () => { + const result = getCloudFormationTemplateUrlFromAgentPolicy(); + expect(result).toBeUndefined(); + }); + + it('should return undefined when selectedPolicy has no package_policies', () => { + const selectedPolicy = {}; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromAgentPolicy(selectedPolicy); + expect(result).toBeUndefined(); + }); + + it('should return undefined when no input has enabled and config.cloud_formation_template_url', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, config: {} }, + { enabled: true, config: {} }, + { enabled: true, config: { other_property: 'value' } }, + ], + }, + { + inputs: [ + { enabled: false, config: {} }, + { enabled: false, config: {} }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromAgentPolicy(selectedPolicy); + expect(result).toBeUndefined(); + }); + + it('should return the first config.cloud_formation_template_url when available', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, config: { cloud_formation_template_url: { value: 'url1' } } }, + { enabled: false, config: { cloud_formation_template_url: { value: 'url2' } } }, + { enabled: false, config: { other_property: 'value' } }, + ], + }, + { + inputs: [ + { enabled: false, config: {} }, + { enabled: true, config: { cloud_formation_template_url: { value: 'url3' } } }, + { enabled: true, config: { cloud_formation_template_url: { value: 'url4' } } }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromAgentPolicy(selectedPolicy); + expect(result).toBe('url3'); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts new file mode 100644 index 0000000000000..81aaf5b3fd970 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts @@ -0,0 +1,38 @@ +/* + * 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 { AgentPolicy } from '../types'; + +/** + * Get the cloud formation template url from a agent policy + * It looks for a config with a cloud_formation_template_url object present in + * the enabled package_policies inputs of the agent policy + */ +export const getCloudFormationTemplateUrlFromAgentPolicy = (selectedPolicy?: AgentPolicy) => { + const cloudFormationTemplateUrl = selectedPolicy?.package_policies?.reduce( + (acc, packagePolicy) => { + const findCloudFormationTemplateUrlConfig = packagePolicy.inputs?.reduce( + (accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.config?.cloud_formation_template_url) { + return input.config.cloud_formation_template_url.value; + } + return accInput; + }, + '' + ); + if (findCloudFormationTemplateUrlConfig) { + return findCloudFormationTemplateUrlConfig; + } + return acc; + }, + '' + ); + return cloudFormationTemplateUrl !== '' ? cloudFormationTemplateUrl : undefined; +}; diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts new file mode 100644 index 0000000000000..523641b10eb1b --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { getCloudFormationTemplateUrlFromPackagePolicy } from './get_cloud_formation_template_url_from_package_policy'; + +describe('getCloudFormationTemplateUrlFromPackagePolicy', () => { + test('returns undefined when packagePolicy is undefined', () => { + const result = getCloudFormationTemplateUrlFromPackagePolicy(undefined); + expect(result).toBeUndefined(); + }); + + test('returns undefined when packagePolicy is defined but inputs are empty', () => { + const packagePolicy = { inputs: [] }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); + expect(result).toBeUndefined(); + }); + + test('returns undefined when no enabled input has a cloudFormationTemplateUrl', () => { + const packagePolicy = { + inputs: [ + { enabled: false, config: { cloud_formation_template_url: { value: 'template1' } } }, + { enabled: false, config: { cloud_formation_template_url: { value: 'template2' } } }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); + expect(result).toBeUndefined(); + }); + + test('returns the cloudFormationTemplateUrl of the first enabled input', () => { + const packagePolicy = { + inputs: [ + { enabled: false, config: { cloud_formation_template_url: { value: 'template1' } } }, + { enabled: true, config: { cloud_formation_template_url: { value: 'template2' } } }, + { enabled: true, config: { cloud_formation_template_url: { value: 'template3' } } }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); + expect(result).toBe('template2'); + }); + + test('returns the cloudFormationTemplateUrl of the first enabled input and ignores subsequent inputs', () => { + const packagePolicy = { + inputs: [ + { enabled: true, config: { cloud_formation_template_url: { value: 'template1' } } }, + { enabled: true, config: { cloud_formation_template_url: { value: 'template2' } } }, + { enabled: true, config: { cloud_formation_template_url: { value: 'template3' } } }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); + expect(result).toBe('template1'); + }); + + // Add more test cases as needed +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts index 0cff589996984..598e71709fdc7 100644 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts @@ -5,31 +5,23 @@ * 2.0. */ -import type { AgentPolicy } from '../types'; +import type { PackagePolicy } from '../types'; /** * Get the cloud formation template url from a package policy * It looks for a config with a cloud_formation_template_url object present in * the enabled inputs of the package policy */ -export const getCloudFormationTemplateUrlFromPackagePolicy = (selectedPolicy?: AgentPolicy) => { - const cloudFormationTemplateUrl = selectedPolicy?.package_policies?.reduce( - (acc, packagePolicy) => { - const findCloudFormationTemplateUrlConfig = packagePolicy.inputs?.reduce( - (accInput, input) => { - if (input?.enabled && input?.config?.cloud_formation_template_url) { - return input.config.cloud_formation_template_url.value; - } - return accInput; - }, - '' - ); - if (findCloudFormationTemplateUrlConfig) { - return findCloudFormationTemplateUrlConfig; - } - return acc; - }, - '' - ); +export const getCloudFormationTemplateUrlFromPackagePolicy = (packagePolicy?: PackagePolicy) => { + const cloudFormationTemplateUrl = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.config?.cloud_formation_template_url) { + return input.config.cloud_formation_template_url.value; + } + return accInput; + }, ''); + return cloudFormationTemplateUrl !== '' ? cloudFormationTemplateUrl : undefined; }; diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 8a71f7d96e1fa..d8e7a3697a66e 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -50,3 +50,4 @@ export { createExtensionRegistrationCallback } from './ui_extensions'; export { incrementPolicyName } from './increment_policy_name'; export { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy'; export { getCloudFormationTemplateUrlFromPackagePolicy } from './get_cloud_formation_template_url_from_package_policy'; +export { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f9e6dcf58d465..15f3613a32eca 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11060,7 +11060,6 @@ "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.multipleUnfollowDescription": "Les index suiveurs seront convertis en index standard. Ils ne seront pas affichés dans la réplication inter-clusters, mais vous pouvez les gérer dans la page Gestion des index. Cette opération ne peut pas être annulée.", "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.singleUnfollowDescription": "L'index suiveur sera converti en index standard. Il ne sera plus affiché dans la réplication inter-clusters, mais vous pouvez le gérer dans la page Gestion des index. Cette opération ne peut pas être annulée.", "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.unfollowSingleTitle": "Annuler le suivi de l'index meneur \"{name}\" ?", - "xpack.csp.awsIntegration.setupInfoContent": "L'intégration nécessitera certaines autorisations AWS en lecture seule pour détecter les erreurs de configuration de la sécurité. Sélectionnez votre méthode préférée pour la fourniture d'informations d'identification AWS que cette intégration utilisera. Vous pouvez suivre ces {stepByStepInstructionsLink} pour générer les informations d'identification nécessaires.", "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " pour \"{name}\"", "xpack.csp.benchmarks.benchmarksTable.errorRenderer.errorDescription": "{error} {statusCode} : {body}", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} many {# intégrations} other {# intégrations}}", @@ -11097,7 +11096,6 @@ "xpack.csp.awsIntegration.roleArnLabel": "Nom ARN de rôle", "xpack.csp.awsIntegration.secretAccessKeyLabel": "Clé d'accès secrète", "xpack.csp.awsIntegration.sessionTokenLabel": "Token de session", - "xpack.csp.awsIntegration.setupInfoContentLink": "instructions pas à pas", "xpack.csp.awsIntegration.setupInfoContentTitle": "Configurer l'accès", "xpack.csp.awsIntegration.sharedCredentialFileLabel": "Fichier d'informations d'identification partagé", "xpack.csp.awsIntegration.sharedCredentialLabel": "Informations d'identification partagées", @@ -15406,10 +15404,8 @@ "xpack.fleet.agentEnrenrollmentStepAgentPolicyollment.noEnrollmentTokensForSelectedPolicyCalloutDescription": "Vous devez créer un token d'inscription afin d'inscrire les agents avec cette politique", "xpack.fleet.agentEnrollment.agentDescription": "Ajoutez des agents Elastic à vos hôtes pour collecter des données et les envoyer à la Suite Elastic.", "xpack.fleet.agentEnrollment.closeFlyoutButtonLabel": "Fermer", - "xpack.fleet.agentEnrollment.cloudFormation.launchButton": "Lancer CloudFormation", "xpack.fleet.agentEnrollment.cloudFormation.loadingAriaLabel": "Chargement des instructions CloudFormation", "xpack.fleet.agentEnrollment.cloudFormation.noApiKey": "Token d'enregistrement non trouvé", - "xpack.fleet.agentEnrollment.cloudFormation.noFleetServer": "Hôte du serveur Fleet non trouvé", "xpack.fleet.agentEnrollment.confirmation.button": "Voir les agents inscrits", "xpack.fleet.agentEnrollment.copyPolicyButton": "Copier dans le presse-papiers", "xpack.fleet.agentEnrollment.downloadDescriptionForK8s": "Copiez ou téléchargez le manifeste Kubernetes.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0a31077bc0905..93cb3b952c5c4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11060,7 +11060,6 @@ "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.multipleUnfollowDescription": "フォロワーインデックスは標準のインデックスに変換されます。今後クラスター横断レプリケーションには表示されませんが、インデックス管理で管理できます。この操作は元に戻すことができません。", "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.singleUnfollowDescription": "フォロワーインデックスは標準のインデックスに変換されます。今後クラスター横断レプリケーションには表示されませんが、インデックス管理で管理できます。この操作は元に戻すことができません。", "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.unfollowSingleTitle": "「{name}」のリーダーインデックスのフォローを解除しますか?", - "xpack.csp.awsIntegration.setupInfoContent": "統合で、セキュリティ構成のエラーを検出するには、特定の読み取り専用AWS権限が必要です。この統合で使用するAWS資格情報を提供するための任意の方法を選択します。必要な資格情報を生成するには、これらの{stepByStepInstructionsLink}に従ってください。", "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " \"{name}\"", "xpack.csp.benchmarks.benchmarksTable.errorRenderer.errorDescription": "{error} {statusCode}: {body}", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}ページを表示中", @@ -11097,7 +11096,6 @@ "xpack.csp.awsIntegration.roleArnLabel": "ロールARN", "xpack.csp.awsIntegration.secretAccessKeyLabel": "シークレットアクセスキー", "xpack.csp.awsIntegration.sessionTokenLabel": "セッショントークン", - "xpack.csp.awsIntegration.setupInfoContentLink": "段階的な手順", "xpack.csp.awsIntegration.setupInfoContentTitle": "アクセスの設定", "xpack.csp.awsIntegration.sharedCredentialFileLabel": "共有資格情報ファイル", "xpack.csp.awsIntegration.sharedCredentialLabel": "共有資格情報", @@ -15405,10 +15403,8 @@ "xpack.fleet.agentEnrenrollmentStepAgentPolicyollment.noEnrollmentTokensForSelectedPolicyCalloutDescription": "エージェントをこのポリシーに登録するには、登録トークンを作成する必要があります", "xpack.fleet.agentEnrollment.agentDescription": "Elastic エージェントをホストに追加し、データを収集して、Elastic Stack に送信します。", "xpack.fleet.agentEnrollment.closeFlyoutButtonLabel": "閉じる", - "xpack.fleet.agentEnrollment.cloudFormation.launchButton": "CloudFormationを起動", "xpack.fleet.agentEnrollment.cloudFormation.loadingAriaLabel": "CloudFormation命令を読み込み中", "xpack.fleet.agentEnrollment.cloudFormation.noApiKey": "登録トークンが見つかりません", - "xpack.fleet.agentEnrollment.cloudFormation.noFleetServer": "Fleetサーバーホスト名が見つかりません", "xpack.fleet.agentEnrollment.confirmation.button": "登録されたエージェントを表示", "xpack.fleet.agentEnrollment.copyPolicyButton": "クリップボードにコピー", "xpack.fleet.agentEnrollment.downloadDescriptionForK8s": "Kubernetesマニフェストをコピーまたはダウンロードします。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2056fe2b0367f..0f0195740e21f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11060,7 +11060,6 @@ "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.multipleUnfollowDescription": "Follower 索引将转换为标准索引。它们不再显示在跨集群复制中,但您可以在“索引管理”中管理它们。此操作无法撤消。", "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.singleUnfollowDescription": "Follower 索引将转换为标准索引。它不再显示在跨集群复制中,但您可以在“索引管理”中管理它。此操作无法撤消。", "xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.unfollowSingleTitle": "取消跟随“{name}”的 Leader 索引?", - "xpack.csp.awsIntegration.setupInfoContent": "此集成需要某些只读 AWS 权限才能检测安全配置错误。选择提供此集成将使用的 AWS 凭据的首选方法。您可以访问这些 {stepByStepInstructionsLink} 以生成必要的凭据。", "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " 对于“{name}”", "xpack.csp.benchmarks.benchmarksTable.errorRenderer.errorDescription": "{error} {statusCode}: {body}", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "正在显示 {pageCount} 个,共 {totalCount, plural, other {# 个集成}} 个", @@ -11097,7 +11096,6 @@ "xpack.csp.awsIntegration.roleArnLabel": "角色 ARN", "xpack.csp.awsIntegration.secretAccessKeyLabel": "机密访问密钥", "xpack.csp.awsIntegration.sessionTokenLabel": "会话令牌", - "xpack.csp.awsIntegration.setupInfoContentLink": "分步说明", "xpack.csp.awsIntegration.setupInfoContentTitle": "设置访问权限", "xpack.csp.awsIntegration.sharedCredentialFileLabel": "共享凭据文件", "xpack.csp.awsIntegration.sharedCredentialLabel": "共享凭据", @@ -15405,10 +15403,8 @@ "xpack.fleet.agentEnrenrollmentStepAgentPolicyollment.noEnrollmentTokensForSelectedPolicyCalloutDescription": "必须创建注册令牌,才能将代理注册到此策略", "xpack.fleet.agentEnrollment.agentDescription": "将 Elastic 代理添加到您的主机,以收集数据并将其发送到 Elastic Stack。", "xpack.fleet.agentEnrollment.closeFlyoutButtonLabel": "关闭", - "xpack.fleet.agentEnrollment.cloudFormation.launchButton": "启动 CloudFormation", "xpack.fleet.agentEnrollment.cloudFormation.loadingAriaLabel": "正在加载 CloudFormation 说明", "xpack.fleet.agentEnrollment.cloudFormation.noApiKey": "找不到注册令牌", - "xpack.fleet.agentEnrollment.cloudFormation.noFleetServer": "找不到 Fleet 服务器主机", "xpack.fleet.agentEnrollment.confirmation.button": "查看注册的代理", "xpack.fleet.agentEnrollment.copyPolicyButton": "复制到剪贴板", "xpack.fleet.agentEnrollment.downloadDescriptionForK8s": "复制或下载 Kubernetes 清单。",