From 56f1621fd56c82e2e7820dbe0541c739766e91e1 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:16:05 +0300 Subject: [PATCH] [Cloud Security] Add support for account type in cspm form (#162413) --- .../aws_credentials_form.tsx | 3 +- .../csp_boxed_radio_group.tsx | 14 +- .../fleet_extensions/eks_credentials_form.tsx | 3 +- .../components/fleet_extensions/mocks.ts | 3 +- .../policy_template_form.test.tsx | 52 ++++++++ .../fleet_extensions/policy_template_form.tsx | 121 +++++++++++++++++- .../post_install_cloud_formation_modal.tsx | 1 + .../cloud_formation_instructions.tsx | 5 + .../steps/compute_steps.tsx | 1 + ...all_cloud_formation_managed_agent_step.tsx | 11 ++ .../hooks/use_create_cloud_formation_url.ts | 28 +++- 11 files changed, 231 insertions(+), 11 deletions(-) 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 index 8110d1f92bbc8..6dd52f259066e 100644 --- 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 @@ -15,6 +15,7 @@ import { EuiTitle, EuiSelect, EuiCallOut, + EuiHorizontalRule, } from '@elastic/eui'; import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import { PackageInfo } from '@kbn/fleet-plugin/common'; @@ -42,7 +43,7 @@ interface AWSSetupInfoContentProps { const AWSSetupInfoContent = ({ integrationLink }: AWSSetupInfoContentProps) => { return ( <> - +

{ +export const RadioGroup = ({ + idSelected, + size, + options, + disabled, + onChange, +}: CspRadioGroupProps) => { const { euiTheme } = useEuiTheme(); return ( 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 index b14cb1cd9cdc7..a5a0be43b350f 100644 --- 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 @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiHorizontalRule, } from '@elastic/eui'; import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; @@ -23,7 +24,7 @@ import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils'; const AWSSetupInfoContent = () => ( <> - +

{ } as PackageInfo; }; -export const getMockPackageInfoCspmAWS = () => { +export const getMockPackageInfoCspmAWS = (packageVersion = '1.5.0') => { return { + version: packageVersion, name: 'cspm', policy_templates: [ { 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 0de4cddf9a29b..71b8b430857a8 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 @@ -712,6 +712,58 @@ describe('', () => { }); describe('AWS Credentials input fields', () => { + it(`renders ${CLOUDBEAT_AWS} Account Type field`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.account_type': { value: 'single_account' }, + }); + + const { getByLabelText } = render( + + ); + + expect(getByLabelText('Single Account')).toBeInTheDocument(); + expect(getByLabelText('AWS Organization')).toBeInTheDocument(); + }); + + it(`${CLOUDBEAT_AWS} form displays upgrade message for unsupported versions and aws organization option is disabled`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'cloud_formation' }, + 'aws.account_type': { value: 'single_account' }, + }); + + const { getByText, getByLabelText } = render( + + ); + + expect( + getByText( + 'AWS Organization not supported in current integration version. Please upgrade to the latest version to enable AWS Organizations integration.' + ) + ).toBeInTheDocument(); + expect(getByLabelText('AWS Organization')).toBeDisabled(); + }); + + it(`${CLOUDBEAT_AWS} form do not displays upgrade message for supported versions and aws organization option is enabled`, () => { + let policy = getMockPolicyAWS(); + policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { + 'aws.credentials.type': { value: 'cloud_formation' }, + 'aws.account_type': { value: 'single_account' }, + }); + + const { queryByText, getByLabelText } = render( + + ); + + expect( + queryByText( + 'AWS Organization not supported in current integration version. Please upgrade to the latest version to enable AWS Organizations integration.' + ) + ).not.toBeInTheDocument(); + expect(getByLabelText('AWS Organization')).toBeEnabled(); + }); + it(`renders ${CLOUDBEAT_AWS} Assume Role fields`, () => { let policy = getMockPolicyAWS(); policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { 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 8c58bbd2dfdf6..c83c2844f68f4 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 @@ -5,13 +5,17 @@ * 2.0. */ import React, { memo, useCallback, useEffect, useState } from 'react'; +import semverCompare from 'semver/functions/compare'; +import semverValid from 'semver/functions/valid'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiLoadingSpinner, EuiSpacer, + EuiText, EuiTitle, } from '@elastic/eui'; import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; @@ -22,6 +26,8 @@ import type { } from '@kbn/fleet-plugin/public/types'; import { PackageInfo, PackagePolicy } from '@kbn/fleet-plugin/common'; import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CspRadioGroupProps, RadioGroup } from './csp_boxed_radio_group'; import { assert } from '../../../common/utils/helpers'; import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types'; import { @@ -73,6 +79,109 @@ interface IntegrationInfoFieldsProps { onChange(field: string, value: string): void; } +type AwsAccountType = 'single_account' | 'organization_account'; + +const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps['options'] => [ + { + id: 'single_account', + label: i18n.translate('xpack.csp.fleetIntegration.awsAccountType.singleAccountLabel', { + defaultMessage: 'Single Account', + }), + }, + { + id: 'organization_account', + label: i18n.translate('xpack.csp.fleetIntegration.awsAccountType.awsOrganizationLabel', { + defaultMessage: 'AWS Organization', + }), + disabled: isAwsOrgDisabled, + tooltip: isAwsOrgDisabled + ? i18n.translate('xpack.csp.fleetIntegration.awsAccountType.awsOrganizationDisabledTooltip', { + defaultMessage: 'Supported from integration version 1.5.0 and above', + }) + : undefined, + }, +]; + +const getAwsAccountType = ( + input: Extract +): AwsAccountType | undefined => input.streams[0].vars?.['aws.account_type']?.value; + +const AWS_ORG_MINIMUM_PACKAGE_VERSION = '1.5.0'; + +const AwsAccountTypeSelect = ({ + input, + newPolicy, + updatePolicy, + packageInfo, +}: { + input: Extract; + newPolicy: NewPackagePolicy; + updatePolicy: (updatedPolicy: NewPackagePolicy) => void; + packageInfo: PackageInfo; +}) => { + // This will disable the aws org option for any version LOWER than 1.5.0 + const isValidSemantic = semverValid(packageInfo.version); + const isAwsOrgDisabled = isValidSemantic + ? semverCompare(packageInfo.version, AWS_ORG_MINIMUM_PACKAGE_VERSION) < 0 + : true; + + const awsAccountTypeOptions = getAwsAccountTypeOptions(isAwsOrgDisabled); + + useEffect(() => { + if (!getAwsAccountType(input)) { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'aws.account_type': { + value: awsAccountTypeOptions[0].id, + type: 'text', + }, + }) + ); + } + // we only wish to call this once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + + + {isAwsOrgDisabled && ( + <> + + + + + + )} + { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'aws.account_type': { + value: accountType, + type: 'text', + }, + }) + ); + }} + size="m" + /> + + + ); +}; + const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) => (
{fields.map(({ value, id, label, error }) => ( @@ -96,7 +205,6 @@ export const CspPolicyTemplateForm = memo + + {/* AWS account type selection box */} + {input.type === 'cloudbeat/cis_aws' && ( + + )} + {/* Defines the name/description */} = ({ enrollmentAPIKey, cloudFormationTemplateUrl, + packagePolicy, }) => { const { isLoading, cloudFormationUrl, error, isError } = useCreateCloudFormationUrl({ enrollmentAPIKey, cloudFormationTemplateUrl, + packagePolicy, }); if (error && isError) { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx index fca803b8785a4..3977cdd5db576 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx @@ -254,6 +254,7 @@ export const ManagedSteps: React.FunctionComponent = ({ selectedApiKeyId, enrollToken, cloudFormationTemplateUrl, + agentPolicy, }) ); } else { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx index b27a54d2149e2..75fec5be125f5 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx @@ -11,9 +11,12 @@ import { i18n } from '@kbn/i18n'; import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; +import type { AgentPolicy } from '../../../../common'; + import type { GetOneEnrollmentAPIKeyResponse } from '../../../../common/types/rest_spec/enrollment_api_key'; import { CloudFormationInstructions } from '../cloud_formation_instructions'; +import { FLEET_CLOUD_SECURITY_POSTURE_PACKAGE } from '../../../../common'; export const InstallCloudFormationManagedAgentStep = ({ selectedApiKeyId, @@ -21,15 +24,22 @@ export const InstallCloudFormationManagedAgentStep = ({ enrollToken, isComplete, cloudFormationTemplateUrl, + agentPolicy, }: { selectedApiKeyId?: string; apiKeyData?: GetOneEnrollmentAPIKeyResponse | null; enrollToken?: string; isComplete?: boolean; cloudFormationTemplateUrl: string; + agentPolicy?: AgentPolicy; }): EuiContainedStepProps => { const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled'; const status = isComplete ? 'complete' : nonCompleteStatus; + + const cloudSecurityPackagePolicy = agentPolicy?.package_policies?.find( + (p) => p.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE + ); + return { status, title: i18n.translate('xpack.fleet.agentEnrollment.cloudFormation.stepEnrollAndRunAgentTitle', { @@ -40,6 +50,7 @@ export const InstallCloudFormationManagedAgentStep = ({ ) : ( 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 index cc76b68b6edb4..861217a272a32 100644 --- 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 @@ -7,20 +7,34 @@ import { i18n } from '@kbn/i18n'; +import type { PackagePolicy, PackagePolicyInput } from '../../common'; + import { useKibanaVersion } from './use_kibana_version'; import { useGetSettings } from './use_request'; +type AwsAccountType = 'single_account' | 'organization_account'; + +const CLOUDBEAT_AWS = 'cloudbeat/cis_aws'; + +const getAwsAccountType = (input?: PackagePolicyInput): AwsAccountType | undefined => + input?.streams[0].vars?.['aws.account_type'].value; + export const useCreateCloudFormationUrl = ({ enrollmentAPIKey, cloudFormationTemplateUrl, + packagePolicy, }: { enrollmentAPIKey: string | undefined; cloudFormationTemplateUrl: string; + packagePolicy?: PackagePolicy; }) => { const { data, isLoading } = useGetSettings(); const kibanaVersion = useKibanaVersion(); + const awsInput = packagePolicy?.inputs?.find((input) => input.type === CLOUDBEAT_AWS); + const awsAccountType = getAwsAccountType(awsInput) || ''; + let isError = false; let error: string | undefined; @@ -47,7 +61,8 @@ export const useCreateCloudFormationUrl = ({ cloudFormationTemplateUrl, enrollmentAPIKey, fleetServerHost, - kibanaVersion + kibanaVersion, + awsAccountType ) : undefined; @@ -63,12 +78,19 @@ const createCloudFormationUrl = ( templateURL: string, enrollmentToken: string, fleetUrl: string, - kibanaVersion: string + kibanaVersion: string, + awsAccountType: string ) => { - const cloudFormationUrl = templateURL + let cloudFormationUrl; + + cloudFormationUrl = templateURL .replace('FLEET_ENROLLMENT_TOKEN', enrollmentToken) .replace('FLEET_URL', fleetUrl) .replace('KIBANA_VERSION', kibanaVersion); + if (cloudFormationUrl.includes('ACCOUNT_TYPE')) { + cloudFormationUrl = cloudFormationUrl.replace('ACCOUNT_TYPE', awsAccountType); + } + return new URL(cloudFormationUrl).toString(); };