Skip to content

Commit

Permalink
[Cloud Security] Add support for account type in cspm form (elastic#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanSh authored Jul 26, 2023
1 parent e17530f commit 56f1621
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,7 +43,7 @@ interface AWSSetupInfoContentProps {
const AWSSetupInfoContent = ({ integrationLink }: AWSSetupInfoContentProps) => {
return (
<>
<EuiSpacer size="l" />
<EuiHorizontalRule margin="xxl" />
<EuiTitle size="s">
<h2>
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@ import React from 'react';
import { useEuiTheme, EuiButton, EuiRadio, EuiToolTip } from '@elastic/eui';
import { css } from '@emotion/react';

interface Props {
export interface CspRadioGroupProps {
disabled?: boolean;
options: RadioOption[];
options: CspRadioOption[];
onChange(id: string): void;
idSelected: string;
size?: 's' | 'm';
}

interface RadioOption {
interface CspRadioOption {
disabled?: boolean;
id: string;
label: string;
icon?: string;
tooltip?: string;
}

export const RadioGroup = ({ idSelected, size, options, disabled, onChange }: Props) => {
export const RadioGroup = ({
idSelected,
size,
options,
disabled,
onChange,
}: CspRadioGroupProps) => {
const { euiTheme } = useEuiTheme();

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,7 +24,7 @@ import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils';

const AWSSetupInfoContent = () => (
<>
<EuiSpacer size="l" />
<EuiHorizontalRule margin="xxl" />
<EuiTitle size="s">
<h2>
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ export const getMockPackageInfoVulnMgmtAWS = () => {
} as PackageInfo;
};

export const getMockPackageInfoCspmAWS = () => {
export const getMockPackageInfoCspmAWS = (packageVersion = '1.5.0') => {
return {
version: packageVersion,
name: 'cspm',
policy_templates: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,58 @@ describe('<CspPolicyTemplateForm />', () => {
});

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(
<WrappedComponent newPolicy={policy} packageInfo={{ version: '1.5.0' } as PackageInfo} />
);

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(
<WrappedComponent newPolicy={policy} packageInfo={{ version: '1.4.0' } as PackageInfo} />
);

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(
<WrappedComponent newPolicy={policy} packageInfo={{ version: '1.5.0' } as PackageInfo} />
);

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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>
): 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<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>;
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 (
<>
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.awsAccountTypeDescriptionLabel"
defaultMessage="Select between single account or organization, and then fill in the name and description to help identify this integration."
/>
</EuiText>
<EuiSpacer size="l" />
{isAwsOrgDisabled && (
<>
<EuiCallOut color="warning">
<FormattedMessage
id="xpack.csp.fleetIntegration.awsAccountType.awsOrganizationNotSupportedMessage"
defaultMessage="AWS Organization not supported in current integration version. Please upgrade to the latest version to enable AWS Organizations integration."
/>
</EuiCallOut>
<EuiSpacer size="l" />
</>
)}
<RadioGroup
idSelected={getAwsAccountType(input) || ''}
options={awsAccountTypeOptions}
onChange={(accountType) => {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
'aws.account_type': {
value: accountType,
type: 'text',
},
})
);
}}
size="m"
/>
<EuiSpacer size="l" />
</>
);
};

const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) => (
<div>
{fields.map(({ value, id, label, error }) => (
Expand All @@ -96,7 +205,6 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
: undefined;
// Handling validation state
const [isValid, setIsValid] = useState(true);

const input = getSelectedOption(newPolicy.inputs, integration);

const updatePolicy = useCallback(
Expand Down Expand Up @@ -230,6 +338,17 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
disabled={isEditPage}
/>
<EuiSpacer size="l" />

{/* AWS account type selection box */}
{input.type === 'cloudbeat/cis_aws' && (
<AwsAccountTypeSelect
input={input}
newPolicy={newPolicy}
updatePolicy={updatePolicy}
packageInfo={packageInfo}
/>
)}

{/* Defines the name/description */}
<IntegrationSettings
fields={integrationFields}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const PostInstallCloudFormationModal: React.FunctionComponent<{
const { cloudFormationUrl, error, isError, isLoading } = useCreateCloudFormationUrl({
cloudFormationTemplateUrl,
enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key,
packagePolicy,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,26 @@ import { EuiButton, EuiSpacer, EuiCallOut, EuiSkeletonText } from '@elastic/eui'
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';

import type { PackagePolicy } from '../../../common';

import { useCreateCloudFormationUrl } from '../../hooks';
import { CloudFormationGuide } from '../cloud_formation_guide';

interface Props {
enrollmentAPIKey?: string;
cloudFormationTemplateUrl: string;
packagePolicy?: PackagePolicy;
}

export const CloudFormationInstructions: React.FunctionComponent<Props> = ({
enrollmentAPIKey,
cloudFormationTemplateUrl,
packagePolicy,
}) => {
const { isLoading, cloudFormationUrl, error, isError } = useCreateCloudFormationUrl({
enrollmentAPIKey,
cloudFormationTemplateUrl,
packagePolicy,
});

if (error && isError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export const ManagedSteps: React.FunctionComponent<InstructionProps> = ({
selectedApiKeyId,
enrollToken,
cloudFormationTemplateUrl,
agentPolicy,
})
);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,35 @@ 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,
apiKeyData,
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', {
Expand All @@ -40,6 +50,7 @@ export const InstallCloudFormationManagedAgentStep = ({
<CloudFormationInstructions
cloudFormationTemplateUrl={cloudFormationTemplateUrl}
enrollmentAPIKey={enrollToken}
packagePolicy={cloudSecurityPackagePolicy}
/>
) : (
<React.Fragment />
Expand Down
Loading

0 comments on commit 56f1621

Please sign in to comment.