[8.9] [CloudSecurity][Fleet] Add CloudFormation install method to CSPM (#159994) (#160559)

# Backport

This will backport the following commits from `main` to `8.9`:
- [[CloudSecurity][Fleet] Add CloudFormation install method to CSPM
(#159994)](https://github.com/elastic/kibana/pull/159994)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Paulo
Henrique","email":"paulo.henrique@elastic.co"},"sourceCommit":{"committedDate":"2023-06-26T18:19:05Z","message":"[CloudSecurity][Fleet]
Add CloudFormation install method to CSPM
(#159994)","sha":"5728bfa1a7514eeb425e0894dab85ba63f48e8fa","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Fleet","release_note:feature","Team:Cloud
Security","backport:prev-minor","ci:cloud-deploy","v8.9.0","v8.10.0"],"number":159994,"url":"https://github.com/elastic/kibana/pull/159994","mergeCommit":{"message":"[CloudSecurity][Fleet]
Add CloudFormation install method to CSPM
(#159994)","sha":"5728bfa1a7514eeb425e0894dab85ba63f48e8fa"}},"sourceBranch":"main","suggestedTargetBranches":["8.9"],"targetPullRequestStates":[{"branch":"8.9","label":"v8.9.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/159994","number":159994,"mergeCommit":{"message":"[CloudSecurity][Fleet]
Add CloudFormation install method to CSPM
(#159994)","sha":"5728bfa1a7514eeb425e0894dab85ba63f48e8fa"}}]}]
BACKPORT-->

Co-authored-by: Paulo Henrique <paulo.henrique@elastic.co>
This commit is contained in:
Kibana Machine 2023-06-26 15:35:54 -04:00 committed by GitHub
parent 4365d08901
commit 34a269f8f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1665 additions and 282 deletions

View file

@ -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<CoreStart>().services;
const query = useQuery<PackagePolicyListData, Error>(PACKAGE_POLICY_LIST_QUERY_KEY, async () => {
try {
const res = await http.get<PackagePolicyListData>(packagePolicyRouteService.getListPath(), {
query: {
perPage: SO_SEARCH_LIMIT,
page: 1,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfoName}`,
},
});
const query = useQuery<PackagePolicyListData, Error>(
PACKAGE_POLICY_LIST_QUERY_KEY,
async () => {
try {
const res = await http.get<PackagePolicyListData>(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;
};

View file

@ -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 (
<>
<EuiSpacer size="l" />
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.csp.awsIntegration.setupInfoContentTitle"
defaultMessage="Setup Access"
/>
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.gettingStarted.setupInfoContent"
defaultMessage="Utilize AWS CloudFormation (a built-in AWS tool) or a series of manual steps to set up and deploy CSPM for assessing your AWS environment's security posture. Refer to our {gettingStartedLink} guide for details."
values={{
gettingStartedLink: (
<EuiLink href={integrationLink} target="_blank">
<FormattedMessage
id="xpack.csp.awsIntegration.gettingStarted.setupInfoContentLink"
defaultMessage="Getting Started"
/>
</EuiLink>
),
}}
/>
</EuiText>
</>
);
};
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<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>;
updatePolicy(updatedPolicy: NewPackagePolicy): void;
packageInfo: PackageInfo;
onChange: any;
setIsValid: (isValid: boolean) => void;
}
const CloudFormationSetup = ({
hasCloudFormationTemplate,
}: {
hasCloudFormationTemplate: boolean;
}) => {
if (!hasCloudFormationTemplate) {
return (
<EuiCallOut color="warning">
<FormattedMessage
id="xpack.csp.awsIntegration.cloudFormationSetupStep.notSupported"
defaultMessage="CloudFormation is not supported on the current Integration version, please upgrade your integration to the latest version to use CloudFormation"
/>
</EuiCallOut>
);
}
return (
<>
<EuiText color="subdued" size="s">
<ol
css={css`
list-style: auto;
`}
>
<li>
<FormattedMessage
id="xpack.csp.awsIntegration.cloudFormationSetupStep.login"
defaultMessage="Log in as an admin to the AWS Account you want to onboard"
/>
</li>
<li>
<FormattedMessage
id="xpack.csp.awsIntegration.cloudFormationSetupStep.save"
defaultMessage="Click the Save and continue button on the bottom right of this page"
/>
</li>
<li>
<FormattedMessage
id="xpack.csp.awsIntegration.cloudFormationSetupStep.launch"
defaultMessage="On the subsequent pop-up modal, click the Launch CloudFormation button."
/>
</li>
</ol>
</EuiText>
<EuiSpacer size="l" />
<ReadDocumentation url={CLOUD_FORMATION_EXTERNAL_DOC_URL} />
</>
);
};
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 }) => (
<EuiLink
href={url}
target="_blank"
rel="noopener nofollow noreferrer"
data-test-subj="externalLink"
>
{children}
</EuiLink>
);
const ReadDocumentation = ({ url }: { url: string }) => {
return (
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.cloudFormationSetupNote"
defaultMessage="Read the {documentation} for more details"
values={{
documentation: (
<Link url={url}>
{i18n.translate('xpack.csp.awsIntegration.documentationLinkText', {
defaultMessage: 'documentation',
})}
</Link>
),
}}
/>
</EuiText>
);
};
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 (
<>
<AWSSetupInfoContent integrationLink={integrationLink} />
<EuiSpacer size="l" />
<RadioGroup
size="m"
options={getSetupFormatOptions()}
idSelected={setupFormat}
onChange={onSetupFormatChange}
/>
<EuiSpacer size="l" />
{setupFormat === 'cloud_formation' && (
<CloudFormationSetup hasCloudFormationTemplate={hasCloudFormationTemplate} />
)}
{setupFormat === 'manual' && (
<>
<AwsCredentialTypeSelector
type={awsCredentialsType}
onChange={(optionId) => {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
'aws.credentials.type': { value: optionId },
})
);
}}
/>
<EuiSpacer size="m" />
{group.info}
<EuiSpacer size="m" />
<ReadDocumentation url={integrationLink} />
<EuiSpacer size="l" />
<AwsInputVarFields
fields={fields}
onChange={(key, value) => {
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }));
}}
/>
</>
)}
<EuiSpacer />
</>
);
};
const AwsCredentialTypeSelector = ({
type,
onChange,
}: {
onChange(type: AwsCredentialsType): void;
type: AwsCredentialsType;
}) => (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.csp.awsIntegration.awsCredentialTypeSelectorLabel', {
defaultMessage: 'Preferred manual method',
})}
>
<EuiSelect
fullWidth
options={getAwsCredentialsFormManualOptions()}
value={type}
onChange={(optionElem) => {
onChange(optionElem.target.value as AwsCredentialsType);
}}
/>
</EuiFormRow>
);
const AwsInputVarFields = ({
fields,
onChange,
}: {
fields: Array<AwsOptions[keyof AwsOptions]['fields'][number] & { value: string; id: string }>;
onChange: (key: string, value: string) => void;
}) => (
<div>
{fields.map((field) => (
<EuiFormRow key={field.id} label={field.label} fullWidth hasChildLabel={true} id={field.id}>
<>
{field.type === 'password' && (
<EuiFieldPassword
id={field.id}
type="dual"
fullWidth
value={field.value || ''}
onChange={(event) => onChange(field.id, event.target.value)}
/>
)}
{field.type === 'text' && (
<EuiFieldText
id={field.id}
fullWidth
value={field.value || ''}
onChange={(event) => onChange(field.id, event.target.value)}
/>
)}
</>
</EuiFormRow>
))}
</div>
);

View file

@ -0,0 +1,176 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
const AssumeRoleDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.assumeRoleDescription"
defaultMessage="An IAM role Amazon Resource Name (ARN) is an IAM identity that you can create in your AWS
account. When creating an IAM role, users can define the roles permissions. Roles do not have
standard long-term credentials such as passwords or access keys."
/>
</EuiText>
</div>
);
const DirectAccessKeysDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.directAccessKeysDescription"
defaultMessage="Access keys are long-term credentials for an IAM user or the AWS account root user."
/>
</EuiText>
</div>
);
const TemporaryKeysDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.temporaryKeysDescription"
defaultMessage="You can configure temporary security credentials in AWS to last for a specified duration. They
consist of an access key ID, a secret access key, and a security token, which is typically
found using GetSessionToken."
/>
</EuiText>
</div>
);
const SharedCredentialsDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.sharedCredentialsDescription"
defaultMessage="If you use different AWS credentials for different tools or applications, you can use profiles
to define multiple access keys in the same configuration file."
/>
</EuiText>
</div>
);
const AWS_FIELD_LABEL = {
access_key_id: i18n.translate('xpack.csp.awsIntegration.accessKeyIdLabel', {
defaultMessage: 'Access Key ID',
}),
secret_access_key: i18n.translate('xpack.csp.awsIntegration.secretAccessKeyLabel', {
defaultMessage: 'Secret Access Key',
}),
};
export type AwsCredentialsType =
| 'assume_role'
| 'direct_access_keys'
| 'temporary_keys'
| 'shared_credentials'
| 'cloud_formation';
export type AwsCredentialsFields = Record<string, { label: string; type?: 'password' | 'text' }>;
export interface AwsOptionValue {
label: string;
info: React.ReactNode;
fields: AwsCredentialsFields;
}
export const getInputVarsFields = (input: NewPackagePolicyInput, fields: AwsCredentialsFields) =>
Object.entries(input.streams[0].vars || {})
.filter(([id]) => id in fields)
.map(([id, inputVar]) => {
const field = fields[id];
return {
id,
label: field.label,
type: field.type || 'text',
value: inputVar.value,
} as const;
});
export type AwsOptions = Record<AwsCredentialsType, AwsOptionValue>;
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: {},
},
});

View file

@ -0,0 +1,205 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useRef } from 'react';
import { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common';
import { cspIntegrationDocsNavigation } from '../../../common/navigation/constants';
import {
getCspmCloudFormationDefaultValue,
getPosturePolicy,
NewPackagePolicyPostureInput,
} from '../utils';
import {
AwsCredentialsType,
DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE,
getAwsCredentialsFormOptions,
getInputVarsFields,
} from './get_aws_credentials_form_options';
import { CLOUDBEAT_AWS } from '../../../../common/constants';
/**
* Update CloudFormation template and stack name in the Agent Policy
* based on the selected policy template
*/
export type SetupFormat = 'cloud_formation' | 'manual';
const getSetupFormatFromInput = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>,
hasCloudFormationTemplate: boolean
): SetupFormat => {
const credentialsType = getAwsCredentialsType(input);
// CloudFormation is the default setup format if the integration has a CloudFormation template
if (!credentialsType && hasCloudFormationTemplate) {
return 'cloud_formation';
}
if (credentialsType !== 'cloud_formation') {
return 'manual';
}
return 'cloud_formation';
};
const getAwsCredentialsType = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>
): AwsCredentialsType | undefined => input.streams[0].vars?.['aws.credentials.type'].value;
export const useAwsCredentialsForm = ({
newPolicy,
input,
packageInfo,
onChange,
setIsValid,
updatePolicy,
}: {
newPolicy: NewPackagePolicy;
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>;
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<string | undefined>(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]);
};

View file

@ -18,60 +18,56 @@ import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CSPM_POLICY_TEMPLATE } from '../../../common/constants';
import { PosturePolicyTemplate } from '../../../common/types';
import { RadioGroup } from './csp_boxed_radio_group';
import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils';
import { cspIntegrationDocsNavigation } from '../../common/navigation/constants';
interface AWSSetupInfoContentProps {
policyTemplate: PosturePolicyTemplate | undefined;
}
const AWSSetupInfoContent = ({ policyTemplate }: AWSSetupInfoContentProps) => {
const { cspm, kspm } = cspIntegrationDocsNavigation;
const integrationLink =
!policyTemplate || policyTemplate === CSPM_POLICY_TEMPLATE
? cspm.getStartedPath
: kspm.getStartedPath;
return (
<>
<EuiSpacer size="l" />
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.csp.awsIntegration.setupInfoContentTitle"
defaultMessage="Setup Access"
/>
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText color={'subdued'} size="s">
const AWSSetupInfoContent = () => (
<>
<EuiSpacer size="l" />
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.csp.awsIntegration.setupInfoContent"
defaultMessage="The integration will require certain read-only AWS permissions to detect security misconfigurations. Select your preferred method of providing the AWS credentials this integration will use. You can follow these {stepByStepInstructionsLink} to generate the necessary credentials."
values={{
stepByStepInstructionsLink: (
<EuiLink href={integrationLink} target="_blank">
<FormattedMessage
id="xpack.csp.awsIntegration.setupInfoContentLink"
defaultMessage="step-by-step instructions"
/>
</EuiLink>
),
}}
id="xpack.csp.eksIntegration.setupInfoContentTitle"
defaultMessage="Setup Access"
/>
</EuiText>
</>
);
};
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.eksIntegration.setupInfoContent"
defaultMessage="The integration will need elevated access to run some CIS benchmark rules. Select your preferred
method of providing the AWS credentials this integration will use. You can follow these
step-by-step instructions to generate the necessary credentials."
/>
</EuiText>
</>
);
const DocsLink = (
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.eksIntegration.docsLink"
defaultMessage="Read the {docs} for more details"
values={{
docs: (
<EuiLink
href="https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html"
external
>
documentation
</EuiLink>
),
}}
/>
</EuiText>
);
const AssumeRoleDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.assumeRoleDescription"
id="xpack.csp.eksIntegration.assumeRoleDescription"
defaultMessage="An IAM role Amazon Resource Name (ARN) is an IAM identity that you can create in your AWS
account. When creating an IAM role, users can define the roles permissions. Roles do not have
standard long-term credentials such as passwords or access keys."
@ -84,7 +80,7 @@ const DirectAccessKeysDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.directAccessKeysDescription"
id="xpack.csp.eksIntegration.directAccessKeysDescription"
defaultMessage="Access keys are long-term credentials for an IAM user or the AWS account root user."
/>
</EuiText>
@ -95,7 +91,7 @@ const TemporaryKeysDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.temporaryKeysDescription"
id="xpack.csp.eksIntegration.temporaryKeysDescription"
defaultMessage="You can configure temporary security credentials in AWS to last for a specified duration. They
consist of an access key ID, a secret access key, and a security token, which is typically
found using GetSessionToken."
@ -108,7 +104,7 @@ const SharedCredentialsDescription = (
<div>
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.awsIntegration.sharedCredentialsDescription"
id="xpack.csp.eksIntegration.sharedCredentialsDescription"
defaultMessage="If you use different AWS credentials for different tools or applications, you can use profiles
to define multiple access keys in the same configuration file."
/>
@ -117,10 +113,10 @@ const SharedCredentialsDescription = (
);
const AWS_FIELD_LABEL = {
access_key_id: i18n.translate('xpack.csp.awsIntegration.accessKeyIdLabel', {
access_key_id: i18n.translate('xpack.csp.eksIntegration.accessKeyIdLabel', {
defaultMessage: 'Access Key ID',
}),
secret_access_key: i18n.translate('xpack.csp.awsIntegration.secretAccessKeyLabel', {
secret_access_key: i18n.translate('xpack.csp.eksIntegration.secretAccessKeyLabel', {
defaultMessage: 'Secret Access Key',
}),
};
@ -136,20 +132,20 @@ type AwsOptions = Record<
const options: AwsOptions = {
assume_role: {
label: i18n.translate('xpack.csp.awsIntegration.assumeRoleLabel', {
label: i18n.translate('xpack.csp.eksIntegration.assumeRoleLabel', {
defaultMessage: 'Assume role',
}),
info: AssumeRoleDescription,
fields: {
role_arn: {
label: i18n.translate('xpack.csp.awsIntegration.roleArnLabel', {
label: i18n.translate('xpack.csp.eksIntegration.roleArnLabel', {
defaultMessage: 'Role ARN',
}),
},
},
},
direct_access_keys: {
label: i18n.translate('xpack.csp.awsIntegration.directAccessKeyLabel', {
label: i18n.translate('xpack.csp.eksIntegration.directAccessKeyLabel', {
defaultMessage: 'Direct access keys',
}),
info: DirectAccessKeysDescription,
@ -160,32 +156,32 @@ const options: AwsOptions = {
},
temporary_keys: {
info: TemporaryKeysDescription,
label: i18n.translate('xpack.csp.awsIntegration.temporaryKeysLabel', {
label: i18n.translate('xpack.csp.eksIntegration.temporaryKeysLabel', {
defaultMessage: 'Temporary keys',
}),
fields: {
access_key_id: { label: AWS_FIELD_LABEL.access_key_id },
secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' },
session_token: {
label: i18n.translate('xpack.csp.awsIntegration.sessionTokenLabel', {
label: i18n.translate('xpack.csp.eksIntegration.sessionTokenLabel', {
defaultMessage: 'Session Token',
}),
},
},
},
shared_credentials: {
label: i18n.translate('xpack.csp.awsIntegration.sharedCredentialLabel', {
label: i18n.translate('xpack.csp.eksIntegration.sharedCredentialLabel', {
defaultMessage: 'Shared credentials',
}),
info: SharedCredentialsDescription,
fields: {
shared_credential_file: {
label: i18n.translate('xpack.csp.awsIntegration.sharedCredentialFileLabel', {
label: i18n.translate('xpack.csp.eksIntegration.sharedCredentialFileLabel', {
defaultMessage: 'Shared Credential File',
}),
},
credential_profile_name: {
label: i18n.translate('xpack.csp.awsIntegration.credentialProfileNameLabel', {
label: i18n.translate('xpack.csp.eksIntegration.credentialProfileNameLabel', {
defaultMessage: 'Credential Profile Name',
}),
},
@ -194,7 +190,7 @@ const options: AwsOptions = {
};
export type AwsCredentialsType = keyof typeof options;
export const DEFAULT_AWS_VARS_GROUP: AwsCredentialsType = 'assume_role';
export const DEFAULT_EKS_VARS_GROUP: AwsCredentialsType = 'assume_role';
const AWS_CREDENTIALS_OPTIONS = Object.keys(options).map((value) => ({
id: value as AwsCredentialsType,
label: options[value as keyof typeof options].label,
@ -225,7 +221,7 @@ const getInputVarsFields = (
const getAwsCredentialsType = (input: Props['input']): AwsCredentialsType | undefined =>
input.streams[0].vars?.['aws.credentials.type'].value;
export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => {
export const EksCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => {
// We only have a value for 'aws.credentials.type' once the form has mounted.
// On initial render we don't have that value so we default to the first option.
const awsCredentialsType = getAwsCredentialsType(input) || AWS_CREDENTIALS_OPTIONS[0].id;
@ -234,7 +230,7 @@ export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) =>
return (
<>
<AWSSetupInfoContent policyTemplate={input.policy_template} />
<AWSSetupInfoContent />
<EuiSpacer size="l" />
<AwsCredentialTypeSelector
type={awsCredentialsType}
@ -248,7 +244,9 @@ export const AwsCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) =>
/>
<EuiSpacer size="m" />
{group.info}
<EuiSpacer size="l" />
<EuiSpacer size="s" />
{DocsLink}
<EuiSpacer />
<AwsInputVarFields
fields={fields}
onChange={(key, value) =>

View file

@ -50,6 +50,34 @@ export const getMockPackageInfoVulnMgmtAWS = () => {
} as PackageInfo;
};
export const getMockPackageInfoCspmAWS = () => {
return {
name: 'cspm',
policy_templates: [
{
title: '',
description: '',
name: 'cspm',
inputs: [
{
type: CLOUDBEAT_AWS,
title: '',
description: '',
vars: [
{
type: 'text',
name: 'cloud_formation_template',
default: 's3_url',
show_user: false,
},
],
},
],
},
],
} as PackageInfo;
};
const getPolicyMock = (
type: PostureInput,
posture: string,
@ -58,6 +86,16 @@ const getPolicyMock = (
const mockPackagePolicy = createNewPackagePolicyMock();
const awsVarsMock = {
access_key_id: { type: 'text' },
secret_access_key: { type: 'text' },
session_token: { type: 'text' },
shared_credential_file: { type: 'text' },
credential_profile_name: { type: 'text' },
role_arn: { type: 'text' },
'aws.credentials.type': { value: 'cloud_formation', type: 'text' },
};
const eksVarsMock = {
access_key_id: { type: 'text' },
secret_access_key: { type: 'text' },
session_token: { type: 'text' },
@ -95,7 +133,7 @@ const getPolicyMock = (
type: CLOUDBEAT_EKS,
policy_template: 'kspm',
enabled: type === CLOUDBEAT_EKS,
streams: [{ enabled: type === CLOUDBEAT_EKS, data_stream: dataStream, vars: awsVarsMock }],
streams: [{ enabled: type === CLOUDBEAT_EKS, data_stream: dataStream, vars: eksVarsMock }],
},
{
type: CLOUDBEAT_AWS,

View file

@ -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('<CspPolicyTemplateForm />', () => {
});
// 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('<CspPolicyTemplateForm />', () => {
});
// 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('<CspPolicyTemplateForm />', () => {
},
});
expect(onChange).toHaveBeenNthCalledWith(3, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyK8s(),
@ -369,7 +370,7 @@ describe('<CspPolicyTemplateForm />', () => {
});
// 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('<CspPolicyTemplateForm />', () => {
});
// 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('<CspPolicyTemplateForm />', () => {
});
// 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('<CspPolicyTemplateForm />', () => {
(useParams as jest.Mock).mockReturnValue({
integration: 'cspm',
});
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
@ -440,11 +442,61 @@ describe('<CspPolicyTemplateForm />', () => {
render(
<WrappedComponent
newPolicy={policy}
packageInfo={{ name: 'cspm' } as PackageInfo}
packageInfo={getMockPackageInfoCspmAWS()}
onChange={onChange}
/>
);
// 1st call happens on mount and selects the CloudFormation template
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
name: 'cloud_security_posture-1',
inputs: policy.inputs.map((input) => {
if (input.type === CLOUDBEAT_AWS) {
return {
...input,
enabled: true,
};
}
return input;
}),
},
});
// 2nd call happens on mount and increments cspm template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
inputs: policy.inputs.map((input) => ({
...input,
enabled: input.policy_template === 'cspm',
})),
name: 'cspm-1',
},
});
// // 3rd call happens on mount and increments cspm template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
inputs: policy.inputs.map((input) => {
if (input.type === CLOUDBEAT_AWS) {
return {
...input,
enabled: true,
config: { cloud_formation_template_url: { value: 's3_url' } },
};
}
return input;
}),
name: 'cloud_security_posture-1',
},
});
onChange({
isValid: true,
updatedPolicy: {
@ -457,30 +509,7 @@ describe('<CspPolicyTemplateForm />', () => {
},
});
// 1st call happens on mount and selects the default policy template enabled input
expect(onChange).toHaveBeenNthCalledWith(1, {
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
name: 'cloud_security_posture-1',
},
});
// 2nd call happens on mount and increments cspm template enabled input
expect(onChange).toHaveBeenNthCalledWith(2, {
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
inputs: policy.inputs.map((input) => ({
...input,
enabled: input.policy_template === 'cspm',
})),
name: 'cspm-1',
},
});
// 3rd call happens on mount and increments cspm template enabled input
expect(onChange).toHaveBeenNthCalledWith(3, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
@ -493,73 +522,69 @@ describe('<CspPolicyTemplateForm />', () => {
});
});
/**
* 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(<WrappedComponent newPolicy={policy} />);
const option = getByLabelText('Assume role');
const option = getByLabelText('Assume role');
expect(option).toBeChecked();
expect(getByLabelText('Role ARN')).toBeInTheDocument();
});
it(`updates ${inputKey} Assume Role fields`, () => {
let policy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`updates ${CLOUDBEAT_EKS} Assume Role fields`, () => {
let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'assume_role' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Role ARN'), 'a');
policy = getPosturePolicy(policy, inputKey, { role_arn: { value: 'a' } });
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { role_arn: { value: 'a' } });
// Ignore 1st call triggered on mount to ensure initial state is valid
expect(onChange).toHaveBeenNthCalledWith(2, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${inputKey} Direct Access Keys fields`, () => {
let policy: NewPackagePolicy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`renders ${CLOUDBEAT_EKS} Direct Access Keys fields`, () => {
let policy: NewPackagePolicy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText } = render(<WrappedComponent newPolicy={policy} />);
const option = getByLabelText('Direct access keys');
const option = getByLabelText('Direct access keys');
expect(option).toBeChecked();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
});
it(`updates ${inputKey} Direct Access Keys fields`, () => {
let policy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`updates ${CLOUDBEAT_EKS} Direct Access Keys fields`, () => {
let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, rerender } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, inputKey, { access_key_id: { value: 'a' } });
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } });
// Ignore 1st call triggered on mount to ensure initial state is valid
expect(onChange).toHaveBeenNthCalledWith(2, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
@ -567,41 +592,43 @@ describe('<CspPolicyTemplateForm />', () => {
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Secret Access Key'), 'b');
policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } });
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } });
expect(onChange).toHaveBeenNthCalledWith(3, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${inputKey} Temporary Keys fields`, () => {
let policy: NewPackagePolicy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`renders ${CLOUDBEAT_EKS} Temporary Keys fields`, () => {
let policy: NewPackagePolicy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText } = render(<WrappedComponent newPolicy={policy} />);
const option = getByLabelText('Temporary keys');
const option = getByLabelText('Temporary keys');
expect(option).toBeChecked();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
expect(getByLabelText('Session Token')).toBeInTheDocument();
});
it(`updates ${inputKey} Temporary Keys fields`, () => {
let policy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`updates ${CLOUDBEAT_EKS} Temporary Keys fields`, () => {
let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, rerender } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, inputKey, { access_key_id: { value: 'a' } });
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } });
// Ignore 1st call triggered on mount to ensure initial state is valid
expect(onChange).toHaveBeenNthCalledWith(2, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
@ -609,9 +636,9 @@ describe('<CspPolicyTemplateForm />', () => {
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Secret Access Key'), 'b');
policy = getPosturePolicy(policy, inputKey, { secret_access_key: { value: 'b' } });
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } });
expect(onChange).toHaveBeenNthCalledWith(3, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
@ -619,42 +646,44 @@ describe('<CspPolicyTemplateForm />', () => {
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Session Token'), 'a');
policy = getPosturePolicy(policy, inputKey, { session_token: { value: 'a' } });
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { session_token: { value: 'a' } });
expect(onChange).toHaveBeenNthCalledWith(4, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${inputKey} Shared Credentials fields`, () => {
let policy: NewPackagePolicy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`renders ${CLOUDBEAT_EKS} Shared Credentials fields`, () => {
let policy: NewPackagePolicy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'shared_credentials' },
});
const { getByLabelText } = render(<WrappedComponent newPolicy={policy} />);
const option = getByLabelText('Shared credentials');
const option = getByLabelText('Shared credentials');
expect(option).toBeChecked();
expect(getByLabelText('Shared Credential File')).toBeInTheDocument();
expect(getByLabelText('Credential Profile Name')).toBeInTheDocument();
});
it(`updates ${inputKey} Shared Credentials fields`, () => {
let policy = getPolicy();
policy = getPosturePolicy(policy, inputKey, {
it(`updates ${CLOUDBEAT_EKS} Shared Credentials fields`, () => {
let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'shared_credentials' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, rerender } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Shared Credential File'), 'a');
policy = getPosturePolicy(policy, inputKey, {
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
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,
});
@ -662,16 +691,195 @@ describe('<CspPolicyTemplateForm />', () => {
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Credential Profile Name'), 'b');
policy = getPosturePolicy(policy, inputKey, {
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
credential_profile_name: { value: 'b' },
});
expect(onChange).toHaveBeenNthCalledWith(3, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
}
});
describe('AWS Credentials input fields', () => {
it(`renders ${CLOUDBEAT_AWS} Assume Role fields`, () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'assume_role' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, getByRole } = render(<WrappedComponent newPolicy={policy} />);
expect(getByRole('option', { name: 'Assume role', selected: true })).toBeInTheDocument();
expect(getByLabelText('Role ARN')).toBeInTheDocument();
});
it(`updates ${CLOUDBEAT_AWS} Assume Role fields`, () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'assume_role' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Role ARN'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { role_arn: { value: 'a' } });
// Ignore 1st call triggered on mount to ensure initial state is valid
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${CLOUDBEAT_AWS} Direct Access Keys fields`, () => {
let policy: NewPackagePolicy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, getByRole } = render(<WrappedComponent newPolicy={policy} />);
expect(
getByRole('option', { name: 'Direct access keys', selected: true })
).toBeInTheDocument();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
});
it(`updates ${CLOUDBEAT_AWS} Direct Access Keys fields`, () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, rerender } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } });
// Ignore 1st call triggered on mount to ensure initial state is valid
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Secret Access Key'), 'b');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } });
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${CLOUDBEAT_AWS} Temporary Keys fields`, () => {
let policy: NewPackagePolicy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, getByRole } = render(<WrappedComponent newPolicy={policy} />);
expect(getByRole('option', { name: 'Temporary keys', selected: true })).toBeInTheDocument();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
expect(getByLabelText('Session Token')).toBeInTheDocument();
});
it(`updates ${CLOUDBEAT_AWS} Temporary Keys fields`, () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, rerender } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } });
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Secret Access Key'), 'b');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } });
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Session Token'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { session_token: { value: 'a' } });
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${CLOUDBEAT_AWS} Shared Credentials fields`, () => {
let policy: NewPackagePolicy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'shared_credentials' },
});
const { getByLabelText, getByRole } = render(<WrappedComponent newPolicy={policy} />);
expect(
getByRole('option', { name: 'Shared credentials', selected: true })
).toBeInTheDocument();
expect(getByLabelText('Shared Credential File')).toBeInTheDocument();
expect(getByLabelText('Credential Profile Name')).toBeInTheDocument();
});
it(`updates ${CLOUDBEAT_AWS} Shared Credentials fields`, () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'shared_credentials' },
'aws.setup.format': { value: 'manual' },
});
const { getByLabelText, rerender } = render(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Shared Credential File'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
shared_credential_file: { value: 'a' },
});
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
rerender(<WrappedComponent newPolicy={policy} />);
userEvent.type(getByLabelText('Credential Profile Name'), 'b');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
credential_profile_name: { value: 'b' },
});
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
});
describe('Vuln Mgmt', () => {
it('Update Agent Policy CloudFormation template from vars', () => {
@ -693,7 +901,7 @@ describe('<CspPolicyTemplateForm />', () => {
}),
};
expect(onChange).toHaveBeenNthCalledWith(2, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});

View file

@ -94,12 +94,14 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
const integration = SUPPORTED_POLICY_TEMPLATES.includes(integrationParam)
? integrationParam
: undefined;
// Handling validation state
const [isValid, setIsValid] = useState(true);
const input = getSelectedOption(newPolicy.inputs, integration);
const updatePolicy = useCallback(
(updatedPolicy: NewPackagePolicy) => 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<PackagePolicyReplaceDefineStepExtensio
*/
const setEnabledPolicyInput = useCallback(
(inputType: PostureInput) => {
const inputVars = getPostureInputHiddenVars(inputType);
const inputVars = getPostureInputHiddenVars(inputType, packageInfo);
const policy = getPosturePolicy(newPolicy, inputType, inputVars);
updatePolicy(policy);
},
[newPolicy, updatePolicy]
[newPolicy, updatePolicy, packageInfo]
);
// search for non null fields of the validation?.vars object
@ -120,6 +122,7 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
);
const [isLoading, setIsLoading] = useState(validationResultsNonNullFields.length > 0);
const [canFetchIntegration, setCanFetchIntegration] = useState(true);
// delaying component rendering due to a race condition issue from Fleet
// TODO: remove this workaround when the following issue is resolved:
@ -133,7 +136,9 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
setTimeout(() => setIsLoading(false), 200);
}, [validationResultsNonNullFields]);
const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name);
const { data: packagePolicyList } = usePackagePolicyList(packageInfo.name, {
enabled: canFetchIntegration,
});
useEffect(() => {
if (isEditPage) return;
@ -161,6 +166,7 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
integration,
newPolicy,
updatePolicy,
setCanFetchIntegration,
});
if (isLoading) {
@ -229,7 +235,14 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
onChange={(field, value) => updatePolicy({ ...newPolicy, [field]: value })}
/>
{/* Defines the vars of the enabled input of the active policy template */}
<PolicyTemplateVarsForm input={input} newPolicy={newPolicy} updatePolicy={updatePolicy} />
<PolicyTemplateVarsForm
input={input}
newPolicy={newPolicy}
updatePolicy={updatePolicy}
packageInfo={packageInfo}
onChange={onChange}
setIsValid={setIsValid}
/>
<EuiSpacer />
</>
);
@ -265,6 +278,7 @@ const usePolicyTemplateInitialName = ({
newPolicy,
packagePolicyList,
updatePolicy,
setCanFetchIntegration,
}: {
isEditPage: boolean;
isLoading: boolean;
@ -272,6 +286,7 @@ const usePolicyTemplateInitialName = ({
newPolicy: NewPackagePolicy;
packagePolicyList: PackagePolicy[] | undefined;
updatePolicy: (policy: NewPackagePolicy) => void;
setCanFetchIntegration: (canFetch: boolean) => void;
}) => {
useEffect(() => {
if (!integration) return;
@ -292,6 +307,7 @@ const usePolicyTemplateInitialName = ({
...newPolicy,
name: currentIntegrationName,
});
setCanFetchIntegration(false);
// since this useEffect should only run on initial mount updatePolicy and newPolicy shouldn't re-trigger it
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, integration, isEditPage, packagePolicyList]);

View file

@ -7,7 +7,8 @@
import React from 'react';
import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import type { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common';
import { PackagePolicyReplaceDefineStepExtensionComponentProps } from '@kbn/fleet-plugin/public/types';
import {
CSPM_POLICY_TEMPLATE,
KSPM_POLICY_TEMPLATE,
@ -17,7 +18,8 @@ import {
import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types';
import { getPolicyTemplateInputOptions, type NewPackagePolicyPostureInput } from './utils';
import { RadioGroup } from './csp_boxed_radio_group';
import { AwsCredentialsForm } from './aws_credentials_form';
import { AwsCredentialsForm } from './aws_credentials_form/aws_credentials_form';
import { EksCredentialsForm } from './eks_credentials_form';
interface PolicyTemplateSelectorProps {
selectedTemplate: CloudSecurityPolicyTemplate;
@ -66,13 +68,17 @@ interface PolicyTemplateVarsFormProps {
newPolicy: NewPackagePolicy;
input: NewPackagePolicyPostureInput;
updatePolicy(updatedPolicy: NewPackagePolicy): void;
packageInfo: PackageInfo;
onChange: PackagePolicyReplaceDefineStepExtensionComponentProps['onChange'];
setIsValid: (isValid: boolean) => void;
}
export const PolicyTemplateVarsForm = ({ input, ...props }: PolicyTemplateVarsFormProps) => {
switch (input.type) {
case 'cloudbeat/cis_aws':
case 'cloudbeat/cis_eks':
return <AwsCredentialsForm {...props} input={input} />;
case 'cloudbeat/cis_eks':
return <EksCredentialsForm {...props} input={input} />;
default:
return null;
}

View file

@ -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(

View file

@ -26,9 +26,10 @@ import {
KSPM_POLICY_TEMPLATE,
VULN_MGMT_POLICY_TEMPLATE,
} from '../../../common/constants';
import { DEFAULT_AWS_VARS_GROUP } from './aws_credentials_form';
import { getDefaultAwsVarsGroup } from './aws_credentials_form/aws_credentials_form';
import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types';
import { cloudPostureIntegrations } from '../../common/constants';
import { DEFAULT_EKS_VARS_GROUP } from './eks_credentials_form';
// Posture policies only support the default namespace
export const POSTURE_NAMESPACE = 'default';
@ -101,7 +102,10 @@ const getPostureInput = (
...(isInputEnabled &&
stream.vars &&
inputVars && {
vars: merge({}, stream.vars, inputVars),
vars: {
...stream.vars,
...inputVars,
},
}),
})),
};
@ -160,14 +164,36 @@ export const getVulnMgmtCloudFormationDefaultValue = (packageInfo: PackageInfo):
return cloudFormationTemplate;
};
export const getCspmCloudFormationDefaultValue = (packageInfo: PackageInfo): string => {
if (!packageInfo.policy_templates) return '';
const policyTemplate = packageInfo.policy_templates.find((p) => p.name === CSPM_POLICY_TEMPLATE);
if (!policyTemplate) return '';
const policyTemplateInputs = hasPolicyTemplateInputs(policyTemplate) && policyTemplate.inputs;
if (!policyTemplateInputs) return '';
const cloudFormationTemplate = policyTemplateInputs.reduce((acc, input): string => {
if (!input.vars) return acc;
const template = input.vars.find((v) => v.name === 'cloud_formation_template')?.default;
return template ? String(template) : acc;
}, '');
return cloudFormationTemplate;
};
/**
* Input vars that are hidden from the user
*/
export const getPostureInputHiddenVars = (inputType: PostureInput) => {
export const getPostureInputHiddenVars = (inputType: PostureInput, packageInfo: PackageInfo) => {
switch (inputType) {
case 'cloudbeat/cis_aws':
return {
'aws.credentials.type': { value: getDefaultAwsVarsGroup(packageInfo), type: 'text' },
};
case 'cloudbeat/cis_eks':
return { 'aws.credentials.type': { value: DEFAULT_AWS_VARS_GROUP } };
return { 'aws.credentials.type': { value: DEFAULT_EKS_VARS_GROUP, type: 'text' } };
default:
return undefined;
}

View file

@ -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 (
<EuiModal data-test-subj="postInstallCloudFormationModal" onClose={onCancel}>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="confirmCloudFormationModalTitleText">
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallCloudFormationModalTitle"
defaultMessage="CloudFormation deployment"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<CloudFormationGuide />
{error && isError && (
<>
<EuiSpacer size="m" />
<EuiCallOut title={error} color="danger" iconType="error" />
</>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty data-test-subj="confirmCloudFormationModalCancelButton" onClick={onCancel}>
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallCloudFormationModal.cancelButton"
defaultMessage="Launch CloudFormation later"
/>
</EuiButtonEmpty>
<EuiButton
data-test-subj="confirmCloudFormationModalConfirmButton"
onClick={() => {
window.open(cloudFormationUrl);
onConfirm();
}}
fill
color="primary"
isLoading={isLoading}
isDisabled={isError}
>
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallCloudFormationModalConfirmButtonLabel"
defaultMessage="Launch CloudFormation"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -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;

View file

@ -58,6 +58,7 @@ import {
import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components';
import { useDevToolsRequest, useOnSubmit } from './hooks';
import { PostInstallCloudFormationModal } from './components/post_install_cloud_formation_modal';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@ -412,6 +413,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
{formState === 'SUBMITTED_CLOUD_FORMATION' && agentPolicy && savedPackagePolicy && (
<PostInstallCloudFormationModal
agentPolicy={agentPolicy}
packagePolicy={savedPackagePolicy}
onConfirm={() => navigateAddAgent(savedPackagePolicy)}
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
{packageInfo && (
<IntegrationBreadcrumb
pkgTitle={integrationInfo?.title || packageInfo.title}

View file

@ -21,7 +21,8 @@ export type PackagePolicyFormState =
| 'CONFIRM'
| 'LOADING'
| 'SUBMITTED'
| 'SUBMITTED_NO_AGENTS';
| 'SUBMITTED_NO_AGENTS'
| 'SUBMITTED_CLOUD_FORMATION';
export interface AddToPolicyParams {
pkgkey: string;

View file

@ -6,78 +6,35 @@
*/
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<Props> = ({
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 (
<>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate('xpack.fleet.agentEnrollment.cloudFormation.noFleetServer', {
defaultMessage: 'Fleet Server host not found',
})}
color="danger"
iconType="error"
/>
</>
);
}
if (!enrollmentAPIKey) {
return (
<>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate('xpack.fleet.agentEnrollment.cloudFormation.noApiKey', {
defaultMessage: 'Enrollment token not found',
})}
color="danger"
iconType="error"
/>
</>
);
}
const cloudFormationUrl = createCloudFormationUrl(
cloudFormationTemplateUrl,
const { isLoading, cloudFormationUrl, error, isError } = useCreateCloudFormationUrl({
enrollmentAPIKey,
fleetServerHost || '',
kibanaVersion
);
cloudFormationTemplateUrl,
});
if (error && isError) {
return (
<>
<EuiSpacer size="m" />
<EuiCallOut title={error} color="danger" iconType="error" />
</>
);
}
return (
<EuiSkeletonText
@ -91,19 +48,15 @@ export const CloudFormationInstructions: React.FunctionComponent<Props> = ({
}
)}
>
<EuiText>
<FormattedMessage
id="xpack.fleet.agentEnrollment.cloudFormation.instructions"
defaultMessage="Sign in to your AWS cloud provider account, and switch to the region that you want to scan, then click Launch CloudFormation."
/>
</EuiText>
<CloudFormationGuide />
<EuiSpacer size="m" />
<EuiButton
color="primary"
fill
target="_blank"
iconSide="right"
iconType="popout"
iconSide="left"
iconType="launch"
fullWidth
href={cloudFormationUrl}
>
<FormattedMessage

View file

@ -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,

View file

@ -0,0 +1,91 @@
/*
* 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 { EuiLink, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
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 }) => (
<EuiLink
href={url}
target="_blank"
rel="noopener nofollow noreferrer"
data-test-subj="externalLink"
>
{children}
</EuiLink>
);
export const CloudFormationGuide = () => {
return (
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.description"
defaultMessage="CloudFormation will create all the necessary resources to evaluate the security posture of your AWS environment. {learnMore}."
values={{
learnMore: (
<Link url={CLOUD_FORMATION_EXTERNAL_DOC_URL}>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.learnMoreLinkText"
defaultMessage="Learn more about CloudFormation"
/>
</Link>
),
}}
/>
</p>
<EuiText size="s" color="subdued">
<ol>
<li>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.steps.login"
defaultMessage="Ensure you are logged in as an admin in the AWS Account you want to onboard"
/>
</li>
<li>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.steps.launch"
defaultMessage="Click the Launch CloudFormation button below."
/>
</li>
<li>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.steps.region"
defaultMessage="(Optional) Change the Amazon region in the upper right corner to the region you want to deploy your stack to"
/>
</li>
<li>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.steps.accept"
defaultMessage="Tick the checkbox under capabilities in the opened CloudFormation stack review form: {acknowledge}"
values={{
acknowledge: (
<strong>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.steps.accept.acknowledge"
defaultMessage="I acknowledge that AWS CloudFormation might create IAM resources."
/>
</strong>
),
}}
/>
</li>
<li>
<FormattedMessage
id="xpack.fleet.cloudFormation.guide.steps.create"
defaultMessage="Click Create stack."
/>
</li>
</ol>
</EuiText>
</EuiText>
);
};

View file

@ -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';

View file

@ -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';

View file

@ -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();
};

View file

@ -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');
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AgentPolicy } from '../types';
/**
* Get the cloud formation template url from a agent policy
* It looks for a config with a cloud_formation_template_url object present in
* the enabled package_policies inputs of the agent policy
*/
export const getCloudFormationTemplateUrlFromAgentPolicy = (selectedPolicy?: AgentPolicy) => {
const cloudFormationTemplateUrl = selectedPolicy?.package_policies?.reduce(
(acc, packagePolicy) => {
const findCloudFormationTemplateUrlConfig = packagePolicy.inputs?.reduce(
(accInput, input) => {
if (accInput !== '') {
return accInput;
}
if (input?.enabled && input?.config?.cloud_formation_template_url) {
return input.config.cloud_formation_template_url.value;
}
return accInput;
},
''
);
if (findCloudFormationTemplateUrlConfig) {
return findCloudFormationTemplateUrlConfig;
}
return acc;
},
''
);
return cloudFormationTemplateUrl !== '' ? cloudFormationTemplateUrl : undefined;
};

View file

@ -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
});

View file

@ -5,31 +5,23 @@
* 2.0.
*/
import type { AgentPolicy } from '../types';
import type { PackagePolicy } from '../types';
/**
* Get the cloud formation template url from a package policy
* It looks for a config with a cloud_formation_template_url object present in
* the enabled inputs of the package policy
*/
export const getCloudFormationTemplateUrlFromPackagePolicy = (selectedPolicy?: AgentPolicy) => {
const cloudFormationTemplateUrl = selectedPolicy?.package_policies?.reduce(
(acc, packagePolicy) => {
const findCloudFormationTemplateUrlConfig = packagePolicy.inputs?.reduce(
(accInput, input) => {
if (input?.enabled && input?.config?.cloud_formation_template_url) {
return input.config.cloud_formation_template_url.value;
}
return accInput;
},
''
);
if (findCloudFormationTemplateUrlConfig) {
return findCloudFormationTemplateUrlConfig;
}
return acc;
},
''
);
export const getCloudFormationTemplateUrlFromPackagePolicy = (packagePolicy?: PackagePolicy) => {
const cloudFormationTemplateUrl = packagePolicy?.inputs?.reduce((accInput, input) => {
if (accInput !== '') {
return accInput;
}
if (input?.enabled && input?.config?.cloud_formation_template_url) {
return input.config.cloud_formation_template_url.value;
}
return accInput;
}, '');
return cloudFormationTemplateUrl !== '' ? cloudFormationTemplateUrl : undefined;
};

View file

@ -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';

View file

@ -11060,7 +11060,6 @@
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.multipleUnfollowDescription": "Les index suiveurs seront convertis en index standard. Ils ne seront pas affichés dans la réplication inter-clusters, mais vous pouvez les gérer dans la page Gestion des index. Cette opération ne peut pas être annulée.",
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.singleUnfollowDescription": "L'index suiveur sera converti en index standard. Il ne sera plus affiché dans la réplication inter-clusters, mais vous pouvez le gérer dans la page Gestion des index. Cette opération ne peut pas être annulée.",
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.unfollowSingleTitle": "Annuler le suivi de l'index meneur \"{name}\" ?",
"xpack.csp.awsIntegration.setupInfoContent": "L'intégration nécessitera certaines autorisations AWS en lecture seule pour détecter les erreurs de configuration de la sécurité. Sélectionnez votre méthode préférée pour la fourniture d'informations d'identification AWS que cette intégration utilisera. Vous pouvez suivre ces {stepByStepInstructionsLink} pour générer les informations d'identification nécessaires.",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " pour \"{name}\"",
"xpack.csp.benchmarks.benchmarksTable.errorRenderer.errorDescription": "{error} {statusCode} : {body}",
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} many {# intégrations} other {# intégrations}}",
@ -11097,7 +11096,6 @@
"xpack.csp.awsIntegration.roleArnLabel": "Nom ARN de rôle",
"xpack.csp.awsIntegration.secretAccessKeyLabel": "Clé d'accès secrète",
"xpack.csp.awsIntegration.sessionTokenLabel": "Token de session",
"xpack.csp.awsIntegration.setupInfoContentLink": "instructions pas à pas",
"xpack.csp.awsIntegration.setupInfoContentTitle": "Configurer l'accès",
"xpack.csp.awsIntegration.sharedCredentialFileLabel": "Fichier d'informations d'identification partagé",
"xpack.csp.awsIntegration.sharedCredentialLabel": "Informations d'identification partagées",
@ -15406,10 +15404,8 @@
"xpack.fleet.agentEnrenrollmentStepAgentPolicyollment.noEnrollmentTokensForSelectedPolicyCalloutDescription": "Vous devez créer un token d'inscription afin d'inscrire les agents avec cette politique",
"xpack.fleet.agentEnrollment.agentDescription": "Ajoutez des agents Elastic à vos hôtes pour collecter des données et les envoyer à la Suite Elastic.",
"xpack.fleet.agentEnrollment.closeFlyoutButtonLabel": "Fermer",
"xpack.fleet.agentEnrollment.cloudFormation.launchButton": "Lancer CloudFormation",
"xpack.fleet.agentEnrollment.cloudFormation.loadingAriaLabel": "Chargement des instructions CloudFormation",
"xpack.fleet.agentEnrollment.cloudFormation.noApiKey": "Token d'enregistrement non trouvé",
"xpack.fleet.agentEnrollment.cloudFormation.noFleetServer": "Hôte du serveur Fleet non trouvé",
"xpack.fleet.agentEnrollment.confirmation.button": "Voir les agents inscrits",
"xpack.fleet.agentEnrollment.copyPolicyButton": "Copier dans le presse-papiers",
"xpack.fleet.agentEnrollment.downloadDescriptionForK8s": "Copiez ou téléchargez le manifeste Kubernetes.",

View file

@ -11060,7 +11060,6 @@
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.multipleUnfollowDescription": "フォロワーインデックスは標準のインデックスに変換されます。今後クラスター横断レプリケーションには表示されませんが、インデックス管理で管理できます。この操作は元に戻すことができません。",
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.singleUnfollowDescription": "フォロワーインデックスは標準のインデックスに変換されます。今後クラスター横断レプリケーションには表示されませんが、インデックス管理で管理できます。この操作は元に戻すことができません。",
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.unfollowSingleTitle": "「{name}」のリーダーインデックスのフォローを解除しますか?",
"xpack.csp.awsIntegration.setupInfoContent": "統合で、セキュリティ構成のエラーを検出するには、特定の読み取り専用AWS権限が必要です。この統合で使用するAWS資格情報を提供するための任意の方法を選択します。必要な資格情報を生成するには、これらの{stepByStepInstructionsLink}に従ってください。",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " \"{name}\"",
"xpack.csp.benchmarks.benchmarksTable.errorRenderer.errorDescription": "{error} {statusCode}: {body}",
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}ページを表示中",
@ -11097,7 +11096,6 @@
"xpack.csp.awsIntegration.roleArnLabel": "ロールARN",
"xpack.csp.awsIntegration.secretAccessKeyLabel": "シークレットアクセスキー",
"xpack.csp.awsIntegration.sessionTokenLabel": "セッショントークン",
"xpack.csp.awsIntegration.setupInfoContentLink": "段階的な手順",
"xpack.csp.awsIntegration.setupInfoContentTitle": "アクセスの設定",
"xpack.csp.awsIntegration.sharedCredentialFileLabel": "共有資格情報ファイル",
"xpack.csp.awsIntegration.sharedCredentialLabel": "共有資格情報",
@ -15405,10 +15403,8 @@
"xpack.fleet.agentEnrenrollmentStepAgentPolicyollment.noEnrollmentTokensForSelectedPolicyCalloutDescription": "エージェントをこのポリシーに登録するには、登録トークンを作成する必要があります",
"xpack.fleet.agentEnrollment.agentDescription": "Elastic エージェントをホストに追加し、データを収集して、Elastic Stack に送信します。",
"xpack.fleet.agentEnrollment.closeFlyoutButtonLabel": "閉じる",
"xpack.fleet.agentEnrollment.cloudFormation.launchButton": "CloudFormationを起動",
"xpack.fleet.agentEnrollment.cloudFormation.loadingAriaLabel": "CloudFormation命令を読み込み中",
"xpack.fleet.agentEnrollment.cloudFormation.noApiKey": "登録トークンが見つかりません",
"xpack.fleet.agentEnrollment.cloudFormation.noFleetServer": "Fleetサーバーホスト名が見つかりません",
"xpack.fleet.agentEnrollment.confirmation.button": "登録されたエージェントを表示",
"xpack.fleet.agentEnrollment.copyPolicyButton": "クリップボードにコピー",
"xpack.fleet.agentEnrollment.downloadDescriptionForK8s": "Kubernetesマニフェストをコピーまたはダウンロードします。",

View file

@ -11060,7 +11060,6 @@
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.multipleUnfollowDescription": "Follower 索引将转换为标准索引。它们不再显示在跨集群复制中,但您可以在“索引管理”中管理它们。此操作无法撤消。",
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.singleUnfollowDescription": "Follower 索引将转换为标准索引。它不再显示在跨集群复制中,但您可以在“索引管理”中管理它。此操作无法撤消。",
"xpack.crossClusterReplication.unfollowLeaderIndex.confirmModal.unfollowSingleTitle": "取消跟随“{name}”的 Leader 索引?",
"xpack.csp.awsIntegration.setupInfoContent": "此集成需要某些只读 AWS 权限才能检测安全配置错误。选择提供此集成将使用的 AWS 凭据的首选方法。您可以访问这些 {stepByStepInstructionsLink} 以生成必要的凭据。",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " 对于“{name}”",
"xpack.csp.benchmarks.benchmarksTable.errorRenderer.errorDescription": "{error} {statusCode}: {body}",
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "正在显示 {pageCount} 个,共 {totalCount, plural, other {# 个集成}} 个",
@ -11097,7 +11096,6 @@
"xpack.csp.awsIntegration.roleArnLabel": "角色 ARN",
"xpack.csp.awsIntegration.secretAccessKeyLabel": "机密访问密钥",
"xpack.csp.awsIntegration.sessionTokenLabel": "会话令牌",
"xpack.csp.awsIntegration.setupInfoContentLink": "分步说明",
"xpack.csp.awsIntegration.setupInfoContentTitle": "设置访问权限",
"xpack.csp.awsIntegration.sharedCredentialFileLabel": "共享凭据文件",
"xpack.csp.awsIntegration.sharedCredentialLabel": "共享凭据",
@ -15405,10 +15403,8 @@
"xpack.fleet.agentEnrenrollmentStepAgentPolicyollment.noEnrollmentTokensForSelectedPolicyCalloutDescription": "必须创建注册令牌,才能将代理注册到此策略",
"xpack.fleet.agentEnrollment.agentDescription": "将 Elastic 代理添加到您的主机,以收集数据并将其发送到 Elastic Stack。",
"xpack.fleet.agentEnrollment.closeFlyoutButtonLabel": "关闭",
"xpack.fleet.agentEnrollment.cloudFormation.launchButton": "启动 CloudFormation",
"xpack.fleet.agentEnrollment.cloudFormation.loadingAriaLabel": "正在加载 CloudFormation 说明",
"xpack.fleet.agentEnrollment.cloudFormation.noApiKey": "找不到注册令牌",
"xpack.fleet.agentEnrollment.cloudFormation.noFleetServer": "找不到 Fleet 服务器主机",
"xpack.fleet.agentEnrollment.confirmation.button": "查看注册的代理",
"xpack.fleet.agentEnrollment.copyPolicyButton": "复制到剪贴板",
"xpack.fleet.agentEnrollment.downloadDescriptionForK8s": "复制或下载 Kubernetes 清单。",