From cb31457fcd7f163ec09e5055aa4ec6486a8e61a5 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:16:36 -0700 Subject: [PATCH 01/25] fix modal dismissing bug --- .../common/api/use_package_policy_list.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) 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..76ee5279fb88c 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; }; From 22f4cb6c4118c9a161bc36a3cc501cdd2621df6a Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:19:14 -0700 Subject: [PATCH 02/25] cloud formation for cspm --- .../fleet_extensions/aws_credentials_form.tsx | 258 +++++++++++++++--- 1 file changed, 218 insertions(+), 40 deletions(-) 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/aws_credentials_form.tsx index a0932d2efd781..e90554d09ee38 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/aws_credentials_form.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiFieldText, EuiFieldPassword, @@ -13,28 +13,29 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiSelect, } from '@elastic/eui'; import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; -import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; +import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { CSPM_POLICY_TEMPLATE } from '../../../common/constants'; +import { CLOUDBEAT_AWS, CSPM_POLICY_TEMPLATE } from '../../../common/constants'; import { PosturePolicyTemplate } from '../../../common/types'; import { RadioGroup } from './csp_boxed_radio_group'; -import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils'; +import { + getCspmCloudFormationDefaultValue, + getPosturePolicy, + NewPackagePolicyPostureInput, +} from './utils'; import { cspIntegrationDocsNavigation } from '../../common/navigation/constants'; interface AWSSetupInfoContentProps { policyTemplate: PosturePolicyTemplate | undefined; + integrationLink: string; } -const AWSSetupInfoContent = ({ policyTemplate }: AWSSetupInfoContentProps) => { - const { cspm, kspm } = cspIntegrationDocsNavigation; - const integrationLink = - !policyTemplate || policyTemplate === CSPM_POLICY_TEMPLATE - ? cspm.getStartedPath - : kspm.getStartedPath; - +const AWSSetupInfoContent = ({ policyTemplate, integrationLink }: AWSSetupInfoContentProps) => { return ( <> @@ -49,14 +50,14 @@ const AWSSetupInfoContent = ({ policyTemplate }: AWSSetupInfoContentProps) => { ), @@ -134,6 +135,21 @@ type AwsOptions = Record< } >; +type SetupFormat = 'cloudFormation' | 'manual'; + +const getSetupFormatOptions = (): Array<{ id: SetupFormat; label: string }> => [ + { + id: 'cloudFormation', + label: 'CloudFormation', + }, + { + id: `manual`, + label: i18n.translate('xpack.csp.awsIntegration.setupFormatOptions.manual', { + defaultMessage: 'Manual', + }), + }, +]; + const options: AwsOptions = { assume_role: { label: i18n.translate('xpack.csp.awsIntegration.assumeRoleLabel', { @@ -196,14 +212,15 @@ const options: AwsOptions = { export type AwsCredentialsType = keyof typeof options; export const DEFAULT_AWS_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, + value: value as AwsCredentialsType, + text: options[value as keyof typeof options].label, })); interface Props { newPolicy: NewPackagePolicy; input: Extract; updatePolicy(updatedPolicy: NewPackagePolicy): void; + packageInfo: PackageInfo; } const getInputVarsFields = ( @@ -225,41 +242,136 @@ const getInputVarsFields = ( const getAwsCredentialsType = (input: Props['input']): AwsCredentialsType | undefined => input.streams[0].vars?.['aws.credentials.type'].value; -export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => { +const CloudFormationSetup = ({ integrationLink }: { integrationLink: string }) => { + return ( + <> + +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
+
+ + + ); +}; + +const ReadDocumentation = ({ integrationLink }: { integrationLink: string }) => { + return ( + + + {i18n.translate('xpack.csp.awsIntegration.documentationLinkText', { + defaultMessage: 'documentation', + })} + + ), + }} + /> + + ); +}; + +export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy, packageInfo }: 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; + const awsCredentialsType = getAwsCredentialsType(input) || AWS_CREDENTIALS_OPTIONS[0].value; const group = options[awsCredentialsType]; const fields = getInputVarsFields(input, group.fields); + const setupFormat: SetupFormat = + input.streams[0].vars?.['aws.setup.format']?.value || 'cloudFormation'; + const { cspm, kspm } = cspIntegrationDocsNavigation; + const integrationLink = + !input.policy_template || input.policy_template === CSPM_POLICY_TEMPLATE + ? cspm.getStartedPath + : kspm.getStartedPath; + + useCloudFormationTemplate({ + packageInfo, + newPolicy, + updatePolicy, + setupFormat, + }); return ( <> - + - + updatePolicy( getPosturePolicy(newPolicy, input.type, { - 'aws.credentials.type': { value: optionId }, + 'aws.setup.format': { value: newSetupFormat }, }) ) } /> - - {group.info} - - updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) - } - /> + {setupFormat === 'cloudFormation' && ( + <> + + + + )} + {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, @@ -267,12 +379,21 @@ const AwsCredentialTypeSelector = ({ onChange(type: AwsCredentialsType): void; type: AwsCredentialsType; }) => ( - onChange(id as AwsCredentialsType)} - /> + + { + onChange(optionElem.target.value as AwsCredentialsType); + }} + /> + ); const AwsInputVarFields = ({ @@ -308,3 +429,60 @@ const AwsInputVarFields = ({ ))} ); + +/** + * Update CloudFormation template and stack name in the Agent Policy + * based on the selected policy template + */ +const useCloudFormationTemplate = ({ + packageInfo, + newPolicy, + updatePolicy, + setupFormat, +}: { + packageInfo: PackageInfo; + newPolicy: NewPackagePolicy; + updatePolicy: (policy: NewPackagePolicy) => void; + setupFormat: SetupFormat; +}) => { + useEffect(() => { + const checkCurrentTemplate = newPolicy?.inputs?.find((i: any) => i.type === CLOUDBEAT_AWS) + ?.config?.cloud_formation_template_url?.value; + + if (setupFormat !== 'cloudFormation') { + if (checkCurrentTemplate !== null) { + updateCloudFormationPolicyTemplate(newPolicy, updatePolicy, null); + } + 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 (checkCurrentTemplate === templateUrl) return; + + updateCloudFormationPolicyTemplate(newPolicy, updatePolicy, templateUrl); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newPolicy?.vars?.cloud_formation_template_url, newPolicy, packageInfo, setupFormat]); +}; + +const updateCloudFormationPolicyTemplate = ( + newPolicy: NewPackagePolicy, + updatePolicy: (policy: NewPackagePolicy) => void, + templateUrl: string | null +) => { + updatePolicy?.({ + ...newPolicy, + inputs: newPolicy.inputs.map((input) => { + if (input.type === CLOUDBEAT_AWS) { + return { + ...input, + config: { cloud_formation_template_url: { value: templateUrl } }, + }; + } + return input; + }), + }); +}; From 5933fc06760b32b1c4ad7e4a61e73d726873bc23 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:20:19 -0700 Subject: [PATCH 03/25] fixing modal dismissing bug --- .../fleet_extensions/policy_template_form.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) 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 d098023b2dc57..57a47531b6502 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 @@ -120,6 +120,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 +134,7 @@ export const CspPolicyTemplateForm = memo setIsLoading(false), 200); }, [validationResultsNonNullFields]); - const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name); + const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name, canFetchIntegration); useEffect(() => { if (isEditPage) return; @@ -161,6 +162,7 @@ export const CspPolicyTemplateForm = memo updatePolicy({ ...newPolicy, [field]: value })} /> {/* Defines the vars of the enabled input of the active policy template */} - + ); @@ -264,6 +271,7 @@ const usePolicyTemplateInitialName = ({ newPolicy, packagePolicyList, updatePolicy, + setCanFetchIntegration, }: { isEditPage: boolean; isLoading: boolean; @@ -271,6 +279,7 @@ const usePolicyTemplateInitialName = ({ newPolicy: NewPackagePolicy; packagePolicyList: PackagePolicy[] | undefined; updatePolicy: (policy: NewPackagePolicy) => void; + setCanFetchIntegration: (canFetch: boolean) => void; }) => { useEffect(() => { if (!integration) return; @@ -286,11 +295,15 @@ const usePolicyTemplateInitialName = ({ if (newPolicy.name === currentIntegrationName) { return; } + updatePolicy({ ...newPolicy, name: currentIntegrationName, }); - }, [isLoading, integration, isEditPage, packagePolicyList, newPolicy, updatePolicy]); + 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 = ( From 4a99c08770f487ba45ed9c0886ece42f39f45ae0 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:21:06 -0700 Subject: [PATCH 04/25] adding packageinfo --- .../components/fleet_extensions/policy_template_selectors.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..d527c2ff8eddc 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,7 @@ 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 { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE, @@ -66,6 +66,7 @@ interface PolicyTemplateVarsFormProps { newPolicy: NewPackagePolicy; input: NewPackagePolicyPostureInput; updatePolicy(updatedPolicy: NewPackagePolicy): void; + packageInfo: PackageInfo; } export const PolicyTemplateVarsForm = ({ input, ...props }: PolicyTemplateVarsFormProps) => { From 8d4e2761dc9e8f1ac69a7832c4949a6e534c6ed6 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:21:34 -0700 Subject: [PATCH 05/25] getCspmCloudFormationDefaultValue --- .../components/fleet_extensions/utils.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 053abaa0f4b34..0d84a47e0e230 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 @@ -160,6 +160,25 @@ 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 */ From 4966ac58e76fa13821713b57220db575d04ea12f Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:22:31 -0700 Subject: [PATCH 06/25] post install cloud formation modal --- .../post_install_cloud_formation_modal.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) 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 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..28c1e1c976d43 --- /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} + > + + + + + ); +}; From 34254054f032481c3a07efa385cb80dbcc21a661 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:25:33 -0700 Subject: [PATCH 07/25] added SUBMITTED_CLOUD_FORMATION to integrations form --- .../single_page_layout/hooks/form.tsx | 16 +++++++++++++++- .../single_page_layout/index.tsx | 9 +++++++++ .../create_package_policy_page/types.ts | 3 ++- 3 files changed, 26 insertions(+), 2 deletions(-) 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 cc01cadccb788..ad4a7651d70e4 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 81c1c518ccd4f..e357e3c92fc9a 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 { @@ -409,6 +410,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} + {formState === 'SUBMITTED_CLOUD_FORMATION' && agentPolicy && savedPackagePolicy && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} {packageInfo && ( Date: Tue, 20 Jun 2023 02:26:15 -0700 Subject: [PATCH 08/25] add reusable cloud formation guide --- .../cloud_formation_instructions.tsx | 78 ++++--------------- .../components/cloud_formation_guide.tsx | 78 +++++++++++++++++++ 2 files changed, 95 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx index 7218e4e526786..2ca42e07b519e 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx @@ -6,79 +6,36 @@ */ import React from 'react'; -import { EuiButton, EuiSpacer, EuiCallOut, EuiSkeletonText, EuiText } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiCallOut, EuiSkeletonText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { useGetSettings, useKibanaVersion } from '../../hooks'; +import { useCreateCloudFormationUrl } from '../../hooks'; +import { CloudFormationGuide } from '../cloud_formation_guide'; interface Props { enrollmentAPIKey?: string; cloudFormationTemplateUrl: string; } -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(); -}; - 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 ( = ({ } )} > - - - + CloudFormation, + }} /> diff --git a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx new file mode 100644 index 0000000000000..0b8860589f285 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx @@ -0,0 +1,78 @@ +/* + * 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 { Link } from 'react-router-dom'; + +export const CloudFormationGuide = () => { + return ( + +

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

+ +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
  7. + + + + ), + }} + /> +
  8. +
  9. + +
  10. +
+
+
+ ); +}; From 5643ebbf6013ca86d71d8dca7776a90cc95f381c Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:27:04 -0700 Subject: [PATCH 09/25] updating get cloud formation methods --- .../agent_enrollment_flyout/hooks.tsx | 4 +-- .../plugins/fleet/public/components/index.ts | 1 + ...ormation_template_url_from_agent_policy.ts | 35 +++++++++++++++++++ ...mation_template_url_from_package_policy.ts | 29 +++++---------- x-pack/plugins/fleet/public/services/index.ts | 1 + 5 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx index c38407568e310..12f49550b5fcb 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx @@ -14,7 +14,7 @@ import { FLEET_CLOUD_SECURITY_POSTURE_PACKAGE, FLEET_CLOUD_DEFEND_PACKAGE, } from '../../../common'; -import { getCloudFormationTemplateUrlFromPackagePolicy } from '../../services'; +import { getCloudFormationTemplateUrlFromAgentPolicy } from '../../services'; import type { K8sMode, CloudSecurityIntegrationType } from './types'; @@ -80,7 +80,7 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) { } const integrationType = getCloudSecurityIntegrationTypeFromPackagePolicy(agentPolicy); - const cloudformationUrl = getCloudFormationTemplateUrlFromPackagePolicy(agentPolicy); + const cloudformationUrl = getCloudFormationTemplateUrlFromAgentPolicy(agentPolicy); return { integrationType, 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/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..1392539246eda --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts @@ -0,0 +1,35 @@ +/* + * 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 (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.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts index 0cff589996984..a80525621ff96 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,20 @@ * 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 (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'; From bf9accdff2ba2665dfc84e0895606225d316fe04 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 02:27:25 -0700 Subject: [PATCH 10/25] add useCreateCloudFormationUrl hook --- x-pack/plugins/fleet/public/hooks/index.ts | 1 + .../hooks/use_create_cloud_formation_url.ts | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts 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(); +}; From e83f5d3440003144b081762e56e0299f56b4fc7d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 04:16:13 -0700 Subject: [PATCH 11/25] updating tests --- .../components/fleet_extensions/mocks.ts | 28 +++++ .../policy_template_form.test.tsx | 104 ++++++++++++------ 2 files changed, 97 insertions(+), 35 deletions(-) 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..82c2b51b9caec 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, 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..9a1a8565df23f 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,7 +442,7 @@ describe('', () => { render( ); @@ -457,17 +459,39 @@ describe('', () => { }, }); - // 1st call happens on mount and selects the default policy template enabled input - expect(onChange).toHaveBeenNthCalledWith(1, { + // 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, + config: { cloud_formation_template_url: { value: 's3_url' } }, + }; + } + return input; + }), }, }); // 2nd call happens on mount and increments cspm template enabled input - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: { + ...getMockPolicyAWS(), + inputs: policy.inputs.map((input) => ({ + ...input, + enabled: input.type === CLOUDBEAT_AWS, + })), + name: 'cloud_security_posture-1', + }, + }); + + // 3rd call happens on mount and increments cspm template enabled input + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), @@ -479,8 +503,8 @@ describe('', () => { }, }); - // 3rd call happens on mount and increments cspm template enabled input - expect(onChange).toHaveBeenNthCalledWith(3, { + // 4th call happens on mount and increments cspm template enabled input + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), @@ -508,12 +532,13 @@ describe('', () => { let policy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { 'aws.credentials.type': { value: 'assume_role' }, + 'aws.setup.format': { value: 'manual' }, }); - const { getByLabelText } = render(); - const option = getByLabelText('Assume role'); + const { getByLabelText, getByRole } = render(); + + expect(getByRole('option', { name: 'Assume role', selected: true })).toBeInTheDocument(); - expect(option).toBeChecked(); expect(getByLabelText('Role ARN')).toBeInTheDocument(); }); @@ -521,6 +546,7 @@ describe('', () => { let policy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { 'aws.credentials.type': { value: 'assume_role' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText } = render(); @@ -528,7 +554,7 @@ describe('', () => { policy = getPosturePolicy(policy, inputKey, { 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, }); @@ -538,12 +564,15 @@ describe('', () => { let policy: NewPackagePolicy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { 'aws.credentials.type': { value: 'direct_access_keys' }, + 'aws.setup.format': { value: 'manual' }, }); - const { getByLabelText } = render(); - const option = getByLabelText('Direct access keys'); + const { getByLabelText, getByRole } = render(); + + expect( + getByRole('option', { name: 'Direct access keys', selected: true }) + ).toBeInTheDocument(); - expect(option).toBeChecked(); expect(getByLabelText('Access Key ID')).toBeInTheDocument(); expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); }); @@ -552,6 +581,7 @@ describe('', () => { let policy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { 'aws.credentials.type': { value: 'direct_access_keys' }, + 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText, rerender } = render(); @@ -559,7 +589,7 @@ describe('', () => { policy = getPosturePolicy(policy, inputKey, { 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, }); @@ -569,7 +599,7 @@ describe('', () => { userEvent.type(getByLabelText('Secret Access Key'), 'b'); policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -579,12 +609,12 @@ describe('', () => { let policy: NewPackagePolicy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { 'aws.credentials.type': { value: 'temporary_keys' }, + 'aws.setup.format': { value: 'manual' }, }); - const { getByLabelText } = render(); - const option = getByLabelText('Temporary keys'); + const { getByLabelText, getByRole } = render(); + expect(getByRole('option', { name: 'Temporary keys', selected: true })).toBeInTheDocument(); - expect(option).toBeChecked(); expect(getByLabelText('Access Key ID')).toBeInTheDocument(); expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); expect(getByLabelText('Session Token')).toBeInTheDocument(); @@ -594,14 +624,14 @@ describe('', () => { let policy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { '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' } }); - // Ignore 1st call triggered on mount to ensure initial state is valid - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -611,7 +641,7 @@ describe('', () => { userEvent.type(getByLabelText('Secret Access Key'), 'b'); policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -621,7 +651,7 @@ describe('', () => { userEvent.type(getByLabelText('Session Token'), 'a'); policy = getPosturePolicy(policy, inputKey, { session_token: { value: 'a' } }); - expect(onChange).toHaveBeenNthCalledWith(4, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -631,12 +661,15 @@ describe('', () => { let policy: NewPackagePolicy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { 'aws.credentials.type': { value: 'shared_credentials' }, + 'aws.setup.format': { value: 'manual' }, }); - const { getByLabelText } = render(); - const option = getByLabelText('Shared credentials'); + const { getByLabelText, getByRole } = render(); + + expect( + getByRole('option', { name: 'Shared credentials', selected: true }) + ).toBeInTheDocument(); - expect(option).toBeChecked(); expect(getByLabelText('Shared Credential File')).toBeInTheDocument(); expect(getByLabelText('Credential Profile Name')).toBeInTheDocument(); }); @@ -645,16 +678,17 @@ describe('', () => { let policy = getPolicy(); policy = getPosturePolicy(policy, inputKey, { '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, { shared_credential_file: { 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, }); @@ -666,7 +700,7 @@ describe('', () => { credential_profile_name: { value: 'b' }, }); - expect(onChange).toHaveBeenNthCalledWith(3, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -693,7 +727,7 @@ describe('', () => { }), }; - expect(onChange).toHaveBeenNthCalledWith(2, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: expectedUpdatedPolicy, }); From 615e50419038de2342e31b6247fdce5d29dd0fd5 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 05:11:19 -0700 Subject: [PATCH 12/25] fixing translations --- x-pack/plugins/translations/translations/fr-FR.json | 4 ---- x-pack/plugins/translations/translations/ja-JP.json | 4 ---- x-pack/plugins/translations/translations/zh-CN.json | 4 ---- 3 files changed, 12 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 62a79c1cff886..0b640afca4c7f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11069,7 +11069,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}}", @@ -11106,7 +11105,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", @@ -15459,10 +15457,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 fbda4b3dfe09c..ef21562f95c15 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11069,7 +11069,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 {#個の統合}}ページを表示中", @@ -11106,7 +11105,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": "共有資格情報", @@ -15458,10 +15456,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 306f183b72169..a67e5f387a891 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11069,7 +11069,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 {# 个集成}} 个", @@ -11106,7 +11105,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": "共享凭据", @@ -15458,10 +15456,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 清单。", From 372729c8815876bfac6d637a5d06ec87a9747a73 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 05:11:36 -0700 Subject: [PATCH 13/25] fixing condition --- .../components/post_install_cloud_formation_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 28c1e1c976d43..33a706f8ca73b 100644 --- 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 @@ -44,7 +44,7 @@ export const PostInstallCloudFormationModal: React.FunctionComponent<{ const { cloudFormationUrl, error, isError, isLoading } = useCreateCloudFormationUrl({ cloudFormationTemplateUrl, - enrollmentAPIKey: apyKeysData?.data?.items[0].api_key, + enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, }); return ( From 16e56e9dc97a39f5fc3ec390bc7e435f61fe42a6 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 05:11:52 -0700 Subject: [PATCH 14/25] fixing typo --- .../plugins/fleet/public/components/cloud_formation_guide.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx index 0b8860589f285..b74c4828f47ac 100644 --- a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx +++ b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx @@ -40,7 +40,7 @@ export const CloudFormationGuide = () => {
  • From fc91a40267d2bd33aa21c5cf38b6fd5a556ad39e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 20 Jun 2023 05:13:09 -0700 Subject: [PATCH 15/25] adding unit tests --- ...ion_template_url_from_agent_policy.test.ts | 68 +++++++++++++++++++ ...ormation_template_url_from_agent_policy.ts | 3 + ...n_template_url_from_package_policy.test.ts | 61 +++++++++++++++++ ...mation_template_url_from_package_policy.ts | 3 + 4 files changed, 135 insertions(+) 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_package_policy.test.ts 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 index 1392539246eda..81aaf5b3fd970 100644 --- 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 @@ -17,6 +17,9 @@ export const getCloudFormationTemplateUrlFromAgentPolicy = (selectedPolicy?: Age (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; } 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 a80525621ff96..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 @@ -14,6 +14,9 @@ import type { PackagePolicy } from '../types'; */ 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; } From 8f8aa465abe2832ef93651f13375c6c3b61f097d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 24 Jun 2023 16:21:43 -0700 Subject: [PATCH 16/25] move eks to it own form --- .../fleet_extensions/eks_credentials_form.tsx | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx new file mode 100644 index 0000000000000..b14cb1cd9cdc7 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx @@ -0,0 +1,308 @@ +/* + * 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, +} from '@elastic/eui'; +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 { RadioGroup } from './csp_boxed_radio_group'; +import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils'; + +const AWSSetupInfoContent = () => ( + <> + + +

    + +

    +
    + + + + + +); + +const DocsLink = ( + + + documentation + + ), + }} + /> + +); + +const AssumeRoleDescription = ( +
    + + + +
    +); + +const DirectAccessKeysDescription = ( +
    + + + +
    +); + +const TemporaryKeysDescription = ( +
    + + + +
    +); + +const SharedCredentialsDescription = ( +
    + + + +
    +); + +const AWS_FIELD_LABEL = { + access_key_id: i18n.translate('xpack.csp.eksIntegration.accessKeyIdLabel', { + defaultMessage: 'Access Key ID', + }), + secret_access_key: i18n.translate('xpack.csp.eksIntegration.secretAccessKeyLabel', { + defaultMessage: 'Secret Access Key', + }), +}; + +type AwsOptions = Record< + 'assume_role' | 'direct_access_keys' | 'temporary_keys' | 'shared_credentials', + { + label: string; + info: React.ReactNode; + fields: Record; + } +>; + +const options: AwsOptions = { + assume_role: { + label: i18n.translate('xpack.csp.eksIntegration.assumeRoleLabel', { + defaultMessage: 'Assume role', + }), + info: AssumeRoleDescription, + fields: { + role_arn: { + label: i18n.translate('xpack.csp.eksIntegration.roleArnLabel', { + defaultMessage: 'Role ARN', + }), + }, + }, + }, + direct_access_keys: { + label: i18n.translate('xpack.csp.eksIntegration.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.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.eksIntegration.sessionTokenLabel', { + defaultMessage: 'Session Token', + }), + }, + }, + }, + shared_credentials: { + label: i18n.translate('xpack.csp.eksIntegration.sharedCredentialLabel', { + defaultMessage: 'Shared credentials', + }), + info: SharedCredentialsDescription, + fields: { + shared_credential_file: { + label: i18n.translate('xpack.csp.eksIntegration.sharedCredentialFileLabel', { + defaultMessage: 'Shared Credential File', + }), + }, + credential_profile_name: { + label: i18n.translate('xpack.csp.eksIntegration.credentialProfileNameLabel', { + defaultMessage: 'Credential Profile Name', + }), + }, + }, + }, +}; + +export type AwsCredentialsType = keyof typeof options; +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, +})); + +interface Props { + newPolicy: NewPackagePolicy; + input: Extract; + updatePolicy(updatedPolicy: NewPackagePolicy): void; +} + +const getInputVarsFields = ( + input: NewPackagePolicyInput, + fields: AwsOptions[keyof AwsOptions]['fields'] +) => + 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; + }); + +const getAwsCredentialsType = (input: Props['input']): AwsCredentialsType | undefined => + input.streams[0].vars?.['aws.credentials.type'].value; + +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; + const group = options[awsCredentialsType]; + const fields = getInputVarsFields(input, group.fields); + + return ( + <> + + + + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'aws.credentials.type': { value: optionId }, + }) + ) + } + /> + + {group.info} + + {DocsLink} + + + updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) + } + /> + + + ); +}; + +const AwsCredentialTypeSelector = ({ + type, + onChange, +}: { + onChange(type: AwsCredentialsType): void; + type: AwsCredentialsType; +}) => ( + onChange(id 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)} + /> + )} + + + ))} +
    +); From a2c7e6613b957f51c5f263284a1dc1300661c2fd Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 24 Jun 2023 16:22:02 -0700 Subject: [PATCH 17/25] update hook parameters --- .../public/common/api/use_package_policy_list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 76ee5279fb88c..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,7 +21,7 @@ interface PackagePolicyListData { const PACKAGE_POLICY_LIST_QUERY_KEY = ['packagePolicyList']; -export const usePackagePolicyList = (packageInfoName: string, enabled = true) => { +export const usePackagePolicyList = (packageInfoName: string, { enabled = true }) => { const { http } = useKibana().services; const query = useQuery( From 91b25d5dd90d75bcc4b870bb20ed9b527352be3d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 24 Jun 2023 16:22:29 -0700 Subject: [PATCH 18/25] updating launch cloud formation texts --- .../cloud_formation_instructions.tsx | 5 +---- .../components/cloud_formation_guide.tsx | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx index 2ca42e07b519e..739e19aacd9b5 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx @@ -61,10 +61,7 @@ export const CloudFormationInstructions: React.FunctionComponent = ({ > CloudFormation, - }} + defaultMessage="Launch CloudFormation" /> diff --git a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx index b74c4828f47ac..afcb985645f7c 100644 --- a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx +++ b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx @@ -6,9 +6,22 @@ */ import React from 'react'; -import { EuiText } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { Link } from 'react-router-dom'; + +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} + +); export const CloudFormationGuide = () => { return ( @@ -19,10 +32,10 @@ export const CloudFormationGuide = () => { defaultMessage="CloudFormation will create all the necessary resources to evaluate the security posture of your AWS environment. {learnMore}." values={{ learnMore: ( - + ), From fcdf7c3fd5ac95502d538755b61f397f34bbe6d1 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 24 Jun 2023 16:23:44 -0700 Subject: [PATCH 19/25] updating utils --- .../public/components/fleet_extensions/utils.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 9d13bec99fbbe..20335f4fffbe3 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'; 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, + }, }), })), }; @@ -182,11 +186,12 @@ export const getCspmCloudFormationDefaultValue = (packageInfo: PackageInfo): str /** * 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) } }; case 'cloudbeat/cis_eks': - return { 'aws.credentials.type': { value: DEFAULT_AWS_VARS_GROUP } }; + return { 'aws.credentials.type': { value: DEFAULT_EKS_VARS_GROUP } }; default: return undefined; } From d17aef4ff6ee9c0e5902da4ce92c62e69cd390bb Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 24 Jun 2023 16:24:10 -0700 Subject: [PATCH 20/25] adding eks form to template selector --- .../fleet_extensions/policy_template_selectors.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d527c2ff8eddc..3aabba70b9ae5 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 @@ -8,6 +8,7 @@ import React from 'react'; import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common'; +import { PackagePolicyReplaceDefineStepExtensionComponentProps } from '@kbn/fleet-plugin/public/types'; import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE, @@ -18,6 +19,7 @@ import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/ import { getPolicyTemplateInputOptions, type NewPackagePolicyPostureInput } from './utils'; import { RadioGroup } from './csp_boxed_radio_group'; import { AwsCredentialsForm } from './aws_credentials_form'; +import { EksCredentialsForm } from './eks_credentials_form'; interface PolicyTemplateSelectorProps { selectedTemplate: CloudSecurityPolicyTemplate; @@ -67,13 +69,16 @@ interface PolicyTemplateVarsFormProps { 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; } From edaebe2acbfbd22ae4e7cf8862400f630f4ae3b2 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 24 Jun 2023 16:25:13 -0700 Subject: [PATCH 21/25] handling validation state in form --- .../fleet_extensions/policy_template_form.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 771494e6fc8df..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 @@ -134,7 +136,9 @@ export const CspPolicyTemplateForm = memo setIsLoading(false), 200); }, [validationResultsNonNullFields]); - const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name, canFetchIntegration); + const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name, { + enabled: canFetchIntegration, + }); useEffect(() => { if (isEditPage) return; @@ -236,6 +240,8 @@ export const CspPolicyTemplateForm = memo From 6bb7d8e91cdb84420681705fea2a0a27f263b1a3 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sun, 25 Jun 2023 21:39:53 -0700 Subject: [PATCH 22/25] splitting files --- .../fleet_extensions/aws_credentials_form.tsx | 488 ------------------ .../aws_credentials_form.tsx | 313 +++++++++++ .../get_aws_credentials_form_options.tsx | 160 ++++++ .../aws_credentials_form/hooks.ts | 221 ++++++++ .../policy_template_selectors.tsx | 2 +- .../components/fleet_extensions/utils.ts | 8 +- 6 files changed, 700 insertions(+), 492 deletions(-) delete mode 100644 x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form.tsx 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 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/aws_credentials_form.tsx deleted file mode 100644 index e90554d09ee38..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form.tsx +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useEffect } from 'react'; -import { - EuiFieldText, - EuiFieldPassword, - EuiFormRow, - EuiLink, - EuiSpacer, - EuiText, - EuiTitle, - EuiSelect, -} from '@elastic/eui'; -import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; -import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; -import { CLOUDBEAT_AWS, CSPM_POLICY_TEMPLATE } from '../../../common/constants'; -import { PosturePolicyTemplate } from '../../../common/types'; -import { RadioGroup } from './csp_boxed_radio_group'; -import { - getCspmCloudFormationDefaultValue, - getPosturePolicy, - NewPackagePolicyPostureInput, -} from './utils'; -import { cspIntegrationDocsNavigation } from '../../common/navigation/constants'; - -interface AWSSetupInfoContentProps { - policyTemplate: PosturePolicyTemplate | undefined; - integrationLink: string; -} - -const AWSSetupInfoContent = ({ policyTemplate, integrationLink }: AWSSetupInfoContentProps) => { - return ( - <> - - -

    - -

    -
    - - - - - - ), - }} - /> - - - ); -}; - -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', - }), -}; - -type AwsOptions = Record< - 'assume_role' | 'direct_access_keys' | 'temporary_keys' | 'shared_credentials', - { - label: string; - info: React.ReactNode; - fields: Record; - } ->; - -type SetupFormat = 'cloudFormation' | 'manual'; - -const getSetupFormatOptions = (): Array<{ id: SetupFormat; label: string }> => [ - { - id: 'cloudFormation', - label: 'CloudFormation', - }, - { - id: `manual`, - label: i18n.translate('xpack.csp.awsIntegration.setupFormatOptions.manual', { - defaultMessage: 'Manual', - }), - }, -]; - -const options: 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', - }), - }, - }, - }, -}; - -export type AwsCredentialsType = keyof typeof options; -export const DEFAULT_AWS_VARS_GROUP: AwsCredentialsType = 'assume_role'; -const AWS_CREDENTIALS_OPTIONS = Object.keys(options).map((value) => ({ - value: value as AwsCredentialsType, - text: options[value as keyof typeof options].label, -})); - -interface Props { - newPolicy: NewPackagePolicy; - input: Extract; - updatePolicy(updatedPolicy: NewPackagePolicy): void; - packageInfo: PackageInfo; -} - -const getInputVarsFields = ( - input: NewPackagePolicyInput, - fields: AwsOptions[keyof AwsOptions]['fields'] -) => - 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; - }); - -const getAwsCredentialsType = (input: Props['input']): AwsCredentialsType | undefined => - input.streams[0].vars?.['aws.credentials.type'].value; - -const CloudFormationSetup = ({ integrationLink }: { integrationLink: string }) => { - return ( - <> - -
      -
    1. - -
    2. -
    3. - -
    4. -
    5. - -
    6. -
    -
    - - - ); -}; - -const ReadDocumentation = ({ integrationLink }: { integrationLink: string }) => { - return ( - - - {i18n.translate('xpack.csp.awsIntegration.documentationLinkText', { - defaultMessage: 'documentation', - })} - - ), - }} - /> - - ); -}; - -export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy, packageInfo }: 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].value; - const group = options[awsCredentialsType]; - const fields = getInputVarsFields(input, group.fields); - const setupFormat: SetupFormat = - input.streams[0].vars?.['aws.setup.format']?.value || 'cloudFormation'; - const { cspm, kspm } = cspIntegrationDocsNavigation; - const integrationLink = - !input.policy_template || input.policy_template === CSPM_POLICY_TEMPLATE - ? cspm.getStartedPath - : kspm.getStartedPath; - - useCloudFormationTemplate({ - packageInfo, - newPolicy, - updatePolicy, - setupFormat, - }); - - return ( - <> - - - - updatePolicy( - getPosturePolicy(newPolicy, input.type, { - 'aws.setup.format': { value: newSetupFormat }, - }) - ) - } - /> - - {setupFormat === 'cloudFormation' && ( - <> - - - - )} - {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)} - /> - )} - - - ))} -
    -); - -/** - * Update CloudFormation template and stack name in the Agent Policy - * based on the selected policy template - */ -const useCloudFormationTemplate = ({ - packageInfo, - newPolicy, - updatePolicy, - setupFormat, -}: { - packageInfo: PackageInfo; - newPolicy: NewPackagePolicy; - updatePolicy: (policy: NewPackagePolicy) => void; - setupFormat: SetupFormat; -}) => { - useEffect(() => { - const checkCurrentTemplate = newPolicy?.inputs?.find((i: any) => i.type === CLOUDBEAT_AWS) - ?.config?.cloud_formation_template_url?.value; - - if (setupFormat !== 'cloudFormation') { - if (checkCurrentTemplate !== null) { - updateCloudFormationPolicyTemplate(newPolicy, updatePolicy, null); - } - 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 (checkCurrentTemplate === templateUrl) return; - - updateCloudFormationPolicyTemplate(newPolicy, updatePolicy, templateUrl); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [newPolicy?.vars?.cloud_formation_template_url, newPolicy, packageInfo, setupFormat]); -}; - -const updateCloudFormationPolicyTemplate = ( - newPolicy: NewPackagePolicy, - updatePolicy: (policy: NewPackagePolicy) => void, - templateUrl: string | null -) => { - updatePolicy?.({ - ...newPolicy, - inputs: newPolicy.inputs.map((input) => { - if (input.type === CLOUDBEAT_AWS) { - return { - ...input, - config: { cloud_formation_template_url: { value: templateUrl } }, - }; - } - return input; - }), - }); -}; 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..6807860f336ab --- /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,160 @@ +/* + * 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'; + +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'; + +interface AwsOptionValue { + label: string; + info: React.ReactNode; + fields: Record; +} + +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..d5e12229a3835 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/hooks.ts @@ -0,0 +1,221 @@ +/* + * 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, NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; +import { cspIntegrationDocsNavigation } from '../../../common/navigation/constants'; +import { + getCspmCloudFormationDefaultValue, + getPosturePolicy, + NewPackagePolicyPostureInput, +} from '../utils'; +import { + AwsCredentialsType, + AwsOptions, + DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE, + getAwsCredentialsFormOptions, +} 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 getInputVarsFields = ( + input: NewPackagePolicyInput, + fields: AwsOptions[keyof AwsOptions]['fields'] +) => + 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; + }); + +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/policy_template_selectors.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx index 3aabba70b9ae5..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 @@ -18,7 +18,7 @@ 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 { 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 20335f4fffbe3..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,7 +26,7 @@ import { KSPM_POLICY_TEMPLATE, VULN_MGMT_POLICY_TEMPLATE, } from '../../../common/constants'; -import { getDefaultAwsVarsGroup } 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'; @@ -189,9 +189,11 @@ export const getCspmCloudFormationDefaultValue = (packageInfo: PackageInfo): str export const getPostureInputHiddenVars = (inputType: PostureInput, packageInfo: PackageInfo) => { switch (inputType) { case 'cloudbeat/cis_aws': - return { 'aws.credentials.type': { value: getDefaultAwsVarsGroup(packageInfo) } }; + return { + 'aws.credentials.type': { value: getDefaultAwsVarsGroup(packageInfo), type: 'text' }, + }; case 'cloudbeat/cis_eks': - return { 'aws.credentials.type': { value: DEFAULT_EKS_VARS_GROUP } }; + return { 'aws.credentials.type': { value: DEFAULT_EKS_VARS_GROUP, type: 'text' } }; default: return undefined; } From 2acb170d7d3ed063d0b4b9180b8bceda8fcf2bdf Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sun, 25 Jun 2023 21:40:03 -0700 Subject: [PATCH 23/25] updating tests --- .../components/fleet_extensions/mocks.ts | 12 +- .../policy_template_form.test.tsx | 300 ++++++++++++++---- .../components/fleet_extensions/utils.test.ts | 2 +- 3 files changed, 249 insertions(+), 65 deletions(-) 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 82c2b51b9caec..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 @@ -86,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' }, @@ -123,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 9a1a8565df23f..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 @@ -447,18 +447,6 @@ describe('', () => { /> ); - onChange({ - isValid: true, - updatedPolicy: { - ...getMockPolicyAWS(), - inputs: policy.inputs.map((input) => ({ - ...input, - enabled: input.policy_template === 'cspm', - })), - name: 'cspm-2', - }, - }); - // 1st call happens on mount and selects the CloudFormation template expect(onChange).toHaveBeenCalledWith({ isValid: true, @@ -469,7 +457,7 @@ describe('', () => { if (input.type === CLOUDBEAT_AWS) { return { ...input, - config: { cloud_formation_template_url: { value: 's3_url' } }, + enabled: true, }; } return input; @@ -484,14 +472,32 @@ describe('', () => { ...getMockPolicyAWS(), inputs: policy.inputs.map((input) => ({ ...input, - enabled: input.type === CLOUDBEAT_AWS, + enabled: input.policy_template === 'cspm', })), - name: 'cloud_security_posture-1', + name: 'cspm-1', }, }); - // 3rd call happens on mount and increments cspm template enabled input + // // 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', + }, + }); + + onChange({ isValid: true, updatedPolicy: { ...getMockPolicyAWS(), @@ -499,11 +505,10 @@ describe('', () => { ...input, enabled: input.policy_template === 'cspm', })), - name: 'cspm-1', + name: 'cspm-2', }, }); - // 4th call happens on mount and increments cspm template enabled input expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: { @@ -517,20 +522,190 @@ 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'); + expect(option).toBeChecked(); + + expect(getByLabelText('Role ARN')).toBeInTheDocument(); + }); + + 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, CLOUDBEAT_EKS, { role_arn: { value: 'a' } }); + + // Ignore 1st call triggered on mount to ensure initial state is valid + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + + 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'); + expect(option).toBeChecked(); + + expect(getByLabelText('Access Key ID')).toBeInTheDocument(); + expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); + }); + + 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, CLOUDBEAT_EKS, { 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_EKS, { secret_access_key: { value: 'b' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + + 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'); + expect(option).toBeChecked(); + + expect(getByLabelText('Access Key ID')).toBeInTheDocument(); + expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); + expect(getByLabelText('Session Token')).toBeInTheDocument(); + }); + + 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, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + + rerender(); + + userEvent.type(getByLabelText('Secret Access Key'), 'b'); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + + rerender(); + + userEvent.type(getByLabelText('Session Token'), 'a'); + policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { session_token: { value: 'a' } }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + + 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'); + expect(option).toBeChecked(); + + expect(getByLabelText('Shared Credential File')).toBeInTheDocument(); + expect(getByLabelText('Credential Profile Name')).toBeInTheDocument(); + }); + + 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, 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' }, }); @@ -542,16 +717,16 @@ describe('', () => { expect(getByLabelText('Role ARN')).toBeInTheDocument(); }); - it(`updates ${inputKey} Assume Role fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + 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, inputKey, { role_arn: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { role_arn: { value: 'a' } }); // Ignore 1st call triggered on mount to ensure initial state is valid expect(onChange).toHaveBeenCalledWith({ @@ -560,9 +735,9 @@ describe('', () => { }); }); - it(`renders ${inputKey} Direct Access Keys fields`, () => { - let policy: NewPackagePolicy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + 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' }, }); @@ -577,16 +752,16 @@ describe('', () => { expect(getByLabelText('Secret Access Key')).toBeInTheDocument(); }); - it(`updates ${inputKey} Direct Access Keys fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + 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, inputKey, { access_key_id: { value: '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({ @@ -597,7 +772,7 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Secret Access Key'), 'b'); - policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } }); expect(onChange).toHaveBeenCalledWith({ isValid: true, @@ -605,9 +780,9 @@ describe('', () => { }); }); - it(`renders ${inputKey} Temporary Keys fields`, () => { - let policy: NewPackagePolicy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + 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' }, }); @@ -620,16 +795,16 @@ describe('', () => { expect(getByLabelText('Session Token')).toBeInTheDocument(); }); - it(`updates ${inputKey} Temporary Keys fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + 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, inputKey, { access_key_id: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } }); expect(onChange).toHaveBeenCalledWith({ isValid: true, @@ -639,7 +814,7 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Secret Access Key'), 'b'); - policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } }); expect(onChange).toHaveBeenCalledWith({ isValid: true, @@ -649,7 +824,7 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Session Token'), 'a'); - policy = getPosturePolicy(policy, inputKey, { session_token: { value: 'a' } }); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { session_token: { value: 'a' } }); expect(onChange).toHaveBeenCalledWith({ isValid: true, @@ -657,11 +832,10 @@ describe('', () => { }); }); - it(`renders ${inputKey} Shared Credentials fields`, () => { - let policy: NewPackagePolicy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + it(`renders ${CLOUDBEAT_AWS} Shared Credentials fields`, () => { + let policy: NewPackagePolicy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { 'aws.credentials.type': { value: 'shared_credentials' }, - 'aws.setup.format': { value: 'manual' }, }); const { getByLabelText, getByRole } = render(); @@ -674,9 +848,9 @@ describe('', () => { expect(getByLabelText('Credential Profile Name')).toBeInTheDocument(); }); - it(`updates ${inputKey} Shared Credentials fields`, () => { - let policy = getPolicy(); - policy = getPosturePolicy(policy, inputKey, { + 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' }, }); @@ -684,7 +858,7 @@ describe('', () => { userEvent.type(getByLabelText('Shared Credential File'), 'a'); - policy = getPosturePolicy(policy, inputKey, { + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { shared_credential_file: { value: 'a' }, }); @@ -696,7 +870,7 @@ describe('', () => { rerender(); userEvent.type(getByLabelText('Credential Profile Name'), 'b'); - policy = getPosturePolicy(policy, inputKey, { + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { credential_profile_name: { value: 'b' }, }); @@ -705,7 +879,7 @@ describe('', () => { updatedPolicy: policy, }); }); - } + }); describe('Vuln Mgmt', () => { it('Update Agent Policy CloudFormation template from vars', () => { 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( From e879af26e9c51929114dec58a6c804e079e201c3 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 26 Jun 2023 00:39:05 -0700 Subject: [PATCH 24/25] CI fix: types --- .../get_aws_credentials_form_options.tsx | 18 ++++++++++++++++- .../aws_credentials_form/hooks.ts | 20 ++----------------- 2 files changed, 19 insertions(+), 19 deletions(-) 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 index 6807860f336ab..f9ebd255880c3 100644 --- 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 @@ -9,6 +9,7 @@ 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 = (
    @@ -75,12 +76,27 @@ export type AwsCredentialsType = | 'shared_credentials' | 'cloud_formation'; +type AwsCredentialsFields = Record; + interface AwsOptionValue { label: string; info: React.ReactNode; - fields: Record; + 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<{ 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 index d5e12229a3835..c689c99b52dfe 100644 --- 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 @@ -6,7 +6,7 @@ */ import { useEffect, useRef } from 'react'; -import { NewPackagePolicy, NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; +import { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common'; import { cspIntegrationDocsNavigation } from '../../../common/navigation/constants'; import { getCspmCloudFormationDefaultValue, @@ -15,9 +15,9 @@ import { } from '../utils'; import { AwsCredentialsType, - AwsOptions, DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE, getAwsCredentialsFormOptions, + getInputVarsFields, } from './get_aws_credentials_form_options'; import { CLOUDBEAT_AWS } from '../../../../common/constants'; /** @@ -43,22 +43,6 @@ const getSetupFormatFromInput = ( return 'cloud_formation'; }; -const getInputVarsFields = ( - input: NewPackagePolicyInput, - fields: AwsOptions[keyof AwsOptions]['fields'] -) => - 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; - }); - const getAwsCredentialsType = ( input: Extract ): AwsCredentialsType | undefined => input.streams[0].vars?.['aws.credentials.type'].value; From 394e93203a812ba028d8e6ef6e4753e50ed5b90b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 26 Jun 2023 08:28:05 -0700 Subject: [PATCH 25/25] Ci fix: exporting type --- .../aws_credentials_form/get_aws_credentials_form_options.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index f9ebd255880c3..4b245b9a992ac 100644 --- 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 @@ -76,9 +76,9 @@ export type AwsCredentialsType = | 'shared_credentials' | 'cloud_formation'; -type AwsCredentialsFields = Record; +export type AwsCredentialsFields = Record; -interface AwsOptionValue { +export interface AwsOptionValue { label: string; info: React.ReactNode; fields: AwsCredentialsFields;