[Cloud Security] Azure - AMR Template form (#166910)

This commit is contained in:
Jordan 2023-09-27 22:35:03 +03:00 committed by GitHub
parent f3f1eec08e
commit 594cb14a28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1370 additions and 124 deletions

View file

@ -10,6 +10,7 @@ import {
VulnSeverity,
AwsCredentialsTypeFieldMap,
GcpCredentialsTypeFieldMap,
AzureCredentialsTypeFieldMap,
} from './types';
export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status';
@ -156,3 +157,8 @@ export const GCP_CREDENTIALS_TYPE_TO_FIELDS_MAP: GcpCredentialsTypeFieldMap = {
'credentials-file': ['gcp.credentials.file'],
'credentials-json': ['gcp.credentials.json'],
};
export const AZURE_CREDENTIALS_TYPE_TO_FIELDS_MAP: AzureCredentialsTypeFieldMap = {
manual: [],
arm_template: [],
};

View file

@ -30,6 +30,12 @@ export type GcpCredentialsTypeFieldMap = {
[key in GcpCredentialsType]: string[];
};
export type AzureCredentialsType = 'arm_template' | 'manual';
export type AzureCredentialsTypeFieldMap = {
[key in AzureCredentialsType]: string[];
};
export type Evaluation = 'passed' | 'failed' | 'NA';
export type PostureTypes = 'cspm' | 'kspm' | 'vuln_mgmt' | 'all';

View file

@ -20,6 +20,7 @@ import {
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP,
GCP_CREDENTIALS_TYPE_TO_FIELDS_MAP,
AZURE_CREDENTIALS_TYPE_TO_FIELDS_MAP,
} from '../constants';
import type {
BenchmarkId,
@ -27,6 +28,7 @@ import type {
BaseCspSetupStatus,
AwsCredentialsType,
GcpCredentialsType,
AzureCredentialsType,
RuleSection,
} from '../types';
@ -119,6 +121,8 @@ export const cleanupCredentials = (packagePolicy: NewPackagePolicy | UpdatePacka
enabledInput?.streams?.[0].vars?.['aws.credentials.type']?.value;
const gcpCredentialType: GcpCredentialsType | undefined =
enabledInput?.streams?.[0].vars?.['gcp.credentials.type']?.value;
const azureCredentialType: AzureCredentialsType | undefined =
enabledInput?.streams?.[0].vars?.['azure.credentials.type']?.value;
if (awsCredentialType || gcpCredentialType) {
let credsToKeep: string[] = [' '];
@ -129,6 +133,9 @@ export const cleanupCredentials = (packagePolicy: NewPackagePolicy | UpdatePacka
} else if (gcpCredentialType) {
credsToKeep = GCP_CREDENTIALS_TYPE_TO_FIELDS_MAP[gcpCredentialType];
credFields = Object.values(GCP_CREDENTIALS_TYPE_TO_FIELDS_MAP).flat();
} else if (azureCredentialType) {
credsToKeep = AZURE_CREDENTIALS_TYPE_TO_FIELDS_MAP[azureCredentialType];
credFields = Object.values(AZURE_CREDENTIALS_TYPE_TO_FIELDS_MAP).flat();
}
if (credsToKeep) {

View file

@ -95,6 +95,7 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = {
icon: googleCloudLogo,
isBeta: true,
},
// needs to be a function that disables/enabled based on integration version
{
type: CLOUDBEAT_AZURE,
name: i18n.translate('xpack.csp.cspmIntegration.azureOption.nameTitle', {
@ -103,11 +104,9 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = {
benchmark: i18n.translate('xpack.csp.cspmIntegration.azureOption.benchmarkTitle', {
defaultMessage: 'CIS Azure',
}),
disabled: true,
disabled: false,
isBeta: true,
icon: 'logoAzure',
tooltip: i18n.translate('xpack.csp.cspmIntegration.azureOption.tooltipContent', {
defaultMessage: 'Coming soon',
}),
},
],
},

View file

@ -0,0 +1,238 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect } from 'react';
import { EuiLink, EuiSpacer, EuiText, EuiTitle, EuiCallOut, EuiHorizontalRule } from '@elastic/eui';
import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import semverValid from 'semver/functions/valid';
import semverCoerce from 'semver/functions/coerce';
import semverLt from 'semver/functions/lt';
import { SetupFormat, useAzureCredentialsForm } from './hooks';
import { NewPackagePolicyPostureInput } from '../utils';
import { CspRadioOption, RadioGroup } from '../csp_boxed_radio_group';
interface AzureSetupInfoContentProps {
integrationLink: string;
}
export const AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE = 'arm_template';
export const AZURE_MANUAL_CREDENTIAL_TYPE = 'manual';
const AzureSetupInfoContent = ({ integrationLink }: AzureSetupInfoContentProps) => {
return (
<>
<EuiHorizontalRule margin="xl" />
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="xpack.csp.azureIntegration.setupInfoContentTitle"
defaultMessage="Setup Access"
/>
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.azureIntegration.gettingStarted.setupInfoContent"
defaultMessage="Utilize an Azure Resource Manager (ARM) template (a built-in Azure IaC tool) or a series of manual steps to set up and deploy CSPM for assessing your Azure environment's security posture. Refer to our {gettingStartedLink} for details."
values={{
gettingStartedLink: (
<EuiLink href={integrationLink} target="_blank">
<FormattedMessage
id="xpack.csp.azureIntegration.gettingStarted.setupInfoContentLink"
defaultMessage="Getting Started guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
</>
);
};
const getSetupFormatOptions = (): CspRadioOption[] => [
{
id: AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE,
label: 'ARM Template',
},
{
id: AZURE_MANUAL_CREDENTIAL_TYPE,
label: i18n.translate('xpack.csp.azureIntegration.setupFormatOptions.manual', {
defaultMessage: 'Manual',
}),
disabled: true,
tooltip: i18n.translate(
'xpack.csp.azureIntegration.setupFormatOptions.manual.disabledTooltip',
{ defaultMessage: 'Coming Soon' }
),
},
];
interface Props {
newPolicy: NewPackagePolicy;
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_azure' }>;
updatePolicy(updatedPolicy: NewPackagePolicy): void;
packageInfo: PackageInfo;
onChange: any;
setIsValid: (isValid: boolean) => void;
}
const ARM_TEMPLATE_EXTERNAL_DOC_URL =
'https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/';
const ArmTemplateSetup = ({
hasArmTemplateUrl,
input,
}: {
hasArmTemplateUrl: boolean;
input: NewPackagePolicyInput;
}) => {
if (!hasArmTemplateUrl) {
return (
<EuiCallOut color="warning">
<FormattedMessage
id="xpack.csp.azureIntegration.armTemplateSetupStep.notSupported"
defaultMessage="ARM Template is not supported on the current Integration version, please upgrade your integration to the latest version to use ARM Template"
/>
</EuiCallOut>
);
}
return (
<>
<EuiText color="subdued" size="s">
<ol
css={css`
list-style: auto;
`}
>
<li>
<FormattedMessage
id="xpack.csp.azureIntegration.armTemplateSetupStep.login"
defaultMessage="Log in to your Azure portal."
/>
</li>
<li>
<FormattedMessage
id="xpack.csp.azureIntegration.armTemplateSetupStep.save"
defaultMessage="Click the Save and continue button on the bottom right of this page."
/>
</li>
<li>
<FormattedMessage
id="xpack.csp.azureIntegration.armTemplateSetupStep.launch"
defaultMessage="On the subsequent pop-up modal, copy the relevant Bash command, then click on the Launch ARM Template button."
/>
</li>
</ol>
</EuiText>
<EuiSpacer size="l" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.azureIntegration.armTemplateSetupNote"
defaultMessage="Read the {documentation} for more details"
values={{
documentation: (
<EuiLink
href={ARM_TEMPLATE_EXTERNAL_DOC_URL}
target="_blank"
rel="noopener nofollow noreferrer"
data-test-subj="externalLink"
>
{i18n.translate('xpack.csp.azureIntegration.documentationLinkText', {
defaultMessage: 'documentation',
})}
</EuiLink>
),
}}
/>
</EuiText>
</>
);
};
const AZURE_MINIMUM_PACKAGE_VERSION = '1.6.0';
export const AzureCredentialsForm = ({
input,
newPolicy,
updatePolicy,
packageInfo,
onChange,
setIsValid,
}: Props) => {
const { setupFormat, onSetupFormatChange, integrationLink, hasArmTemplateUrl } =
useAzureCredentialsForm({
newPolicy,
input,
packageInfo,
onChange,
setIsValid,
updatePolicy,
});
useEffect(() => {
if (!setupFormat) {
onSetupFormatChange(AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE);
}
}, [setupFormat, onSetupFormatChange]);
const packageSemanticVersion = semverValid(packageInfo.version);
const cleanPackageVersion = semverCoerce(packageSemanticVersion) || '';
const isPackageVersionValidForAzure = !semverLt(
cleanPackageVersion,
AZURE_MINIMUM_PACKAGE_VERSION
);
useEffect(() => {
setIsValid(isPackageVersionValidForAzure);
onChange({
isValid: isPackageVersionValidForAzure,
updatedPolicy: newPolicy,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, packageInfo, setupFormat]);
if (!isPackageVersionValidForAzure) {
return (
<>
<EuiSpacer size="l" />
<EuiCallOut color="warning">
<FormattedMessage
id="xpack.csp.azureIntegration.azureNotSupportedMessage"
defaultMessage="CIS Azure is not supported on the current Integration version, please upgrade your integration to the latest version to use CIS Azure"
/>
</EuiCallOut>
</>
);
}
return (
<>
<AzureSetupInfoContent integrationLink={integrationLink} />
<EuiSpacer size="l" />
<RadioGroup
size="m"
options={getSetupFormatOptions()}
idSelected={setupFormat}
onChange={(idSelected: SetupFormat) =>
idSelected !== setupFormat && onSetupFormatChange(idSelected)
}
/>
<EuiSpacer size="l" />
{setupFormat === AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE && (
<ArmTemplateSetup hasArmTemplateUrl={hasArmTemplateUrl} input={input} />
)}
<EuiSpacer />
</>
);
};

View file

@ -0,0 +1,51 @@
/*
* 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 { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { AzureCredentialsType } from '../../../../common/types';
export type AzureCredentialsFields = Record<string, { label: string; type?: 'password' | 'text' }>;
export interface AzureOptionValue {
label: string;
info: React.ReactNode;
fields: AzureCredentialsFields;
}
export type AzureOptions = Record<AzureCredentialsType, AzureOptionValue>;
export const getInputVarsFields = (input: NewPackagePolicyInput, fields: AzureCredentialsFields) =>
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 const DEFAULT_AZURE_MANUAL_CREDENTIALS_TYPE = 'manual';
export const getAzureCredentialsFormOptions = (): AzureOptions => ({
arm_template: {
label: 'ARM Template',
info: [],
fields: {},
},
manual: {
label: i18n.translate('xpack.csp.azureIntegration.credentialType.manualLabel', {
defaultMessage: 'Manual',
}),
info: [],
fields: {},
},
});

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 { useEffect, useRef } from 'react';
import { NewPackagePolicy, PackageInfo } from '@kbn/fleet-plugin/common';
import { AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE } from './azure_credentials_form';
import { cspIntegrationDocsNavigation } from '../../../common/navigation/constants';
import {
getArmTemplateUrlFromCspmPackage,
getPosturePolicy,
NewPackagePolicyPostureInput,
} from '../utils';
import {
DEFAULT_AZURE_MANUAL_CREDENTIALS_TYPE,
getAzureCredentialsFormOptions,
getInputVarsFields,
} from './get_azure_credentials_form_options';
import { CLOUDBEAT_AZURE } from '../../../../common/constants';
import { AzureCredentialsType } from '../../../../common/types';
export type SetupFormat = AzureCredentialsType;
const getAzureCredentialsType = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_azure' }>
): AzureCredentialsType | undefined => input.streams[0].vars?.['azure.credentials.type']?.value;
const getAzureArmTemplateUrl = (newPolicy: NewPackagePolicy) => {
const template: string | undefined = newPolicy?.inputs?.find((i) => i.type === CLOUDBEAT_AZURE)
?.config?.arm_template_url?.value;
return template || undefined;
};
const updateAzureArmTemplateUrlInPolicy = (
newPolicy: NewPackagePolicy,
updatePolicy: (policy: NewPackagePolicy) => void,
templateUrl: string | undefined
) => {
updatePolicy?.({
...newPolicy,
inputs: newPolicy.inputs.map((input) => {
if (input.type === CLOUDBEAT_AZURE) {
return {
...input,
config: { arm_template_url: { value: templateUrl } },
};
}
return input;
}),
});
};
const useUpdateAzureArmTemplate = ({
packageInfo,
newPolicy,
updatePolicy,
setupFormat,
}: {
packageInfo: PackageInfo;
newPolicy: NewPackagePolicy;
updatePolicy: (policy: NewPackagePolicy) => void;
setupFormat: SetupFormat;
}) => {
useEffect(() => {
const azureArmTemplateUrl = getAzureArmTemplateUrl(newPolicy);
if (setupFormat === 'manual') {
if (!!azureArmTemplateUrl) {
updateAzureArmTemplateUrlInPolicy(newPolicy, updatePolicy, undefined);
}
return;
}
const templateUrl = getArmTemplateUrlFromCspmPackage(packageInfo);
if (templateUrl === '') return;
if (azureArmTemplateUrl === templateUrl) return;
updateAzureArmTemplateUrlInPolicy(newPolicy, updatePolicy, templateUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newPolicy?.vars?.arm_template_url, newPolicy, packageInfo, setupFormat]);
};
export const useAzureCredentialsForm = ({
newPolicy,
input,
packageInfo,
onChange,
setIsValid,
updatePolicy,
}: {
newPolicy: NewPackagePolicy;
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_azure' }>;
packageInfo: PackageInfo;
onChange: (opts: any) => void;
setIsValid: (isValid: boolean) => void;
updatePolicy: (updatedPolicy: NewPackagePolicy) => void;
}) => {
const azureCredentialsType: AzureCredentialsType =
getAzureCredentialsType(input) || AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE;
const options = getAzureCredentialsFormOptions();
const hasArmTemplateUrl = !!getArmTemplateUrlFromCspmPackage(packageInfo);
const setupFormat = azureCredentialsType;
const group = options[azureCredentialsType];
const fields = getInputVarsFields(input, group.fields);
const fieldsSnapshot = useRef({});
const lastManualCredentialsType = useRef<string | undefined>(undefined);
useEffect(() => {
const isInvalid = setupFormat === AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE && !hasArmTemplateUrl;
setIsValid(!isInvalid);
onChange({
isValid: !isInvalid,
updatedPolicy: newPolicy,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setupFormat, input.type]);
const integrationLink = cspIntegrationDocsNavigation.cspm.getStartedPath;
useUpdateAzureArmTemplate({
packageInfo,
newPolicy,
updatePolicy,
setupFormat,
});
const onSetupFormatChange = (newSetupFormat: SetupFormat) => {
if (newSetupFormat === AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE) {
fieldsSnapshot.current = Object.fromEntries(
fields?.map((field) => [field.id, { value: field.value }])
);
lastManualCredentialsType.current = getAzureCredentialsType(input);
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
'azure.credentials.type': {
value: AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE,
type: 'text',
},
...Object.fromEntries(fields?.map((field) => [field.id, { value: undefined }])),
})
);
} else {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
'azure.credentials.type': {
value: lastManualCredentialsType.current || DEFAULT_AZURE_MANUAL_CREDENTIALS_TYPE,
type: 'text',
},
...fieldsSnapshot.current,
})
);
}
};
return {
azureCredentialsType,
setupFormat,
group,
fields,
integrationLink,
hasArmTemplateUrl,
onSetupFormatChange,
};
};

View file

@ -17,7 +17,7 @@ export interface CspRadioGroupProps {
size?: 's' | 'm';
}
interface CspRadioOption {
export interface CspRadioOption {
disabled?: boolean;
id: string;
label: string;

View file

@ -19,6 +19,7 @@ import type { PostureInput } from '../../../common/types';
export const getMockPolicyAWS = () => getPolicyMock(CLOUDBEAT_AWS, 'cspm', 'aws');
export const getMockPolicyGCP = () => getPolicyMock(CLOUDBEAT_GCP, 'cspm', 'gcp');
export const getMockPolicyAzure = () => getPolicyMock(CLOUDBEAT_AZURE, 'cspm', 'azure');
export const getMockPolicyK8s = () => getPolicyMock(CLOUDBEAT_VANILLA, 'kspm', 'self_managed');
export const getMockPolicyEKS = () => getPolicyMock(CLOUDBEAT_EKS, 'kspm', 'eks');
export const getMockPolicyVulnMgmtAWS = () =>
@ -102,6 +103,28 @@ export const getMockPackageInfoCspmGCP = (packageVersion = '1.5.2') => {
} as PackageInfo;
};
export const getMockPackageInfoCspmAzure = (packageVersion = '1.6.0') => {
return {
version: packageVersion,
name: 'cspm',
policy_templates: [
{
title: '',
description: '',
name: 'cspm',
inputs: [
{
type: CLOUDBEAT_AZURE,
title: 'Azure',
description: '',
vars: [{}],
},
],
},
],
} as PackageInfo;
};
const getPolicyMock = (
type: PostureInput,
posture: string,
@ -136,6 +159,11 @@ const getPolicyMock = (
'gcp.credentials.type': { type: 'text' },
};
const azureVarsMock = {
'azure.account_type': { type: 'text' },
'azure.credentials.type': { type: 'text' },
};
const dataStream = { type: 'logs', dataset: 'cloud_security_posture.findings' };
return {
@ -182,7 +210,9 @@ const getPolicyMock = (
type: CLOUDBEAT_AZURE,
policy_template: 'cspm',
enabled: false,
streams: [{ enabled: false, data_stream: dataStream }],
streams: [
{ enabled: type === CLOUDBEAT_AZURE, data_stream: dataStream, vars: azureVarsMock },
],
},
{
type: CLOUDBEAT_VULN_MGMT_AWS,

View file

@ -14,9 +14,11 @@ import {
import { TestProvider } from '../../test/test_provider';
import {
getMockPackageInfoCspmAWS,
getMockPackageInfoCspmAzure,
getMockPackageInfoCspmGCP,
getMockPackageInfoVulnMgmtAWS,
getMockPolicyAWS,
getMockPolicyAzure,
getMockPolicyEKS,
getMockPolicyGCP,
getMockPolicyK8s,
@ -30,7 +32,12 @@ import type {
} from '@kbn/fleet-plugin/common';
import userEvent from '@testing-library/user-event';
import { getPosturePolicy } from './utils';
import { CLOUDBEAT_AWS, CLOUDBEAT_EKS, CLOUDBEAT_GCP } from '../../../common/constants';
import {
CLOUDBEAT_AWS,
CLOUDBEAT_AZURE,
CLOUDBEAT_EKS,
CLOUDBEAT_GCP,
} from '../../../common/constants';
import { useParams } from 'react-router-dom';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
@ -205,7 +212,7 @@ describe('<CspPolicyTemplateForm />', () => {
expect(option3).toBeInTheDocument();
expect(option1).toBeEnabled();
expect(option2).toBeEnabled();
expect(option3).toBeDisabled();
expect(option3).toBeEnabled();
expect(option1).toBeChecked();
});
@ -1130,4 +1137,44 @@ describe('<CspPolicyTemplateForm />', () => {
});
});
});
describe('Azure Credentials input fields', () => {
it(`renders ${CLOUDBEAT_AZURE} Not supported when version is not at least version 1.6.0`, () => {
let policy = getMockPolicyAzure();
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.type': { value: 'arm_template' },
'azure.account_type': { value: 'single-account-azure' },
});
const { getByText } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmAzure('1.5.0')} />
);
expect(onChange).toHaveBeenCalledWith({
isValid: false,
updatedPolicy: policy,
});
expect(
getByText(
'CIS Azure is not supported on the current Integration version, please upgrade your integration to the latest version to use CIS Azure'
)
).toBeInTheDocument();
});
it(`selects default ${CLOUDBEAT_AZURE} fields`, () => {
let policy = getMockPolicyAzure();
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.type': { value: 'arm_template' },
'azure.account_type': { value: 'single-account-azure' },
});
render(<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmAzure()} />);
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
});
});

View file

@ -27,6 +27,7 @@ import type {
import { PackageInfo, PackagePolicy } from '@kbn/fleet-plugin/common';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE } from './azure_credentials_form/azure_credentials_form';
import { CspRadioGroupProps, RadioGroup } from './csp_boxed_radio_group';
import { assert } from '../../../common/utils/helpers';
import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types';
@ -83,7 +84,11 @@ export const AWS_SINGLE_ACCOUNT = 'single-account';
export const AWS_ORGANIZATION_ACCOUNT = 'organization-account';
export const GCP_SINGLE_ACCOUNT = 'single-account-gcp';
export const GCP_ORGANIZATION_ACCOUNT = 'organization-account-gcp';
export const AZURE_SINGLE_ACCOUNT = 'single-account-azure';
export const AZURE_ORGANIZATION_ACCOUNT = 'organization-account-azure';
type AwsAccountType = typeof AWS_SINGLE_ACCOUNT | typeof AWS_ORGANIZATION_ACCOUNT;
type AzureAccountType = typeof AZURE_SINGLE_ACCOUNT | typeof AZURE_ORGANIZATION_ACCOUNT;
const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps['options'] => [
{
@ -128,6 +133,28 @@ const getGcpAccountTypeOptions = (): CspRadioGroupProps['options'] => [
},
];
const getAzureAccountTypeOptions = (): CspRadioGroupProps['options'] => [
{
id: AZURE_ORGANIZATION_ACCOUNT,
label: i18n.translate('xpack.csp.fleetIntegration.azureAccountType.azureOrganizationLabel', {
defaultMessage: 'Azure Organization',
}),
disabled: true,
tooltip: i18n.translate(
'xpack.csp.fleetIntegration.azureAccountType.azureOrganizationDisabledTooltip',
{
defaultMessage: 'Coming Soon',
}
),
},
{
id: AZURE_SINGLE_ACCOUNT,
label: i18n.translate('xpack.csp.fleetIntegration.azureAccountType.singleAccountLabel', {
defaultMessage: 'Single Subscription',
}),
},
];
const getAwsAccountType = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>
): AwsAccountType | undefined => input.streams[0].vars?.['aws.account_type']?.value;
@ -175,7 +202,7 @@ const AwsAccountTypeSelect = ({
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.awsAccountTypeDescriptionLabel"
defaultMessage="Select between single account or organization."
defaultMessage="Select between single account or organization, and then fill in the name and description to help identify this integration."
/>
</EuiText>
<EuiSpacer size="l" />
@ -277,6 +304,89 @@ const GcpAccountTypeSelect = ({
);
};
const getAzureAccountType = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_azure' }>
): AzureAccountType | undefined => input.streams[0].vars?.['azure.account_type']?.value;
const AzureAccountTypeSelect = ({
input,
newPolicy,
updatePolicy,
}: {
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_azure' }>;
newPolicy: NewPackagePolicy;
updatePolicy: (updatedPolicy: NewPackagePolicy) => void;
}) => {
const azureAccountTypeOptions = getAzureAccountTypeOptions();
useEffect(() => {
if (!getAzureAccountType(input)) {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
'azure.account_type': {
value: AZURE_SINGLE_ACCOUNT,
type: 'text',
},
'azure.credentials.type': {
value: AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE,
type: 'text',
},
})
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, updatePolicy]);
return (
<>
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.azureAccountTypeDescriptionLabel"
defaultMessage="Select between onboarding an Azure Organization (tenant root group) or a single Azure subscription, and then fill in the name and description to help identify this integration."
/>
</EuiText>
<EuiSpacer size="l" />
<RadioGroup
idSelected={getAzureAccountType(input) || ''}
options={azureAccountTypeOptions}
onChange={(accountType) => {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
'azure.account_type': {
value: accountType,
type: 'text',
},
})
);
}}
size="m"
/>
{getAzureAccountType(input) === AZURE_ORGANIZATION_ACCOUNT && (
<>
<EuiSpacer size="l" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.azureAccountType.azureOrganizationDescription"
defaultMessage="Connect Elastic to every Azure Subscription (current and future) in your environment by providing Elastic with read-only (configuration) access to your Azure Organization (tenant root group)."
/>
</EuiText>
</>
)}
{getAzureAccountType(input) === AZURE_SINGLE_ACCOUNT && (
<>
<EuiSpacer size="l" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.azureAccountType.singleAccountDescription"
defaultMessage="Deploying to a single subscription is suitable for an initial POC. To ensure compete coverage, it is strongly recommended to deploy CSPM at the organization (tenant root group) level, which automatically connects all subscriptions (both current and future)."
/>
</EuiText>
</>
)}
</>
);
};
const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) => (
<div>
{fields.map(({ value, id, label, error }) => (
@ -303,7 +413,9 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
const input = getSelectedOption(newPolicy.inputs, integration);
const updatePolicy = useCallback(
(updatedPolicy: NewPackagePolicy) => onChange({ isValid, updatedPolicy }),
(updatedPolicy: NewPackagePolicy) => {
onChange({ isValid, updatedPolicy });
},
[onChange, isValid]
);
/**
@ -434,13 +546,6 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
/>
<EuiSpacer size="l" />
{/* Defines the name/description */}
<IntegrationSettings
fields={integrationFields}
onChange={(field, value) => updatePolicy({ ...newPolicy, [field]: value })}
/>
<EuiSpacer size="l" />
{/* AWS account type selection box */}
{input.type === 'cloudbeat/cis_aws' && (
<AwsAccountTypeSelect
@ -460,6 +565,17 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
/>
)}
{input.type === 'cloudbeat/cis_azure' && (
<AzureAccountTypeSelect input={input} newPolicy={newPolicy} updatePolicy={updatePolicy} />
)}
{/* Defines the name/description */}
<EuiSpacer size="l" />
<IntegrationSettings
fields={integrationFields}
onChange={(field, value) => updatePolicy({ ...newPolicy, [field]: value })}
/>
{/* Defines the vars of the enabled input of the active policy template */}
<PolicyTemplateVarsForm
input={input}

View file

@ -18,6 +18,7 @@ import {
import type { PostureInput, CloudSecurityPolicyTemplate } from '../../../common/types';
import { getPolicyTemplateInputOptions, type NewPackagePolicyPostureInput } from './utils';
import { RadioGroup } from './csp_boxed_radio_group';
import { AzureCredentialsForm } from './azure_credentials_form/azure_credentials_form';
import { AwsCredentialsForm } from './aws_credentials_form/aws_credentials_form';
import { EksCredentialsForm } from './eks_credentials_form';
import { GcpCredentialsForm } from './gcp_credential_form';
@ -82,6 +83,8 @@ export const PolicyTemplateVarsForm = ({ input, ...props }: PolicyTemplateVarsFo
return <EksCredentialsForm {...props} input={input} />;
case 'cloudbeat/cis_gcp':
return <GcpCredentialsForm {...props} input={input} />;
case 'cloudbeat/cis_azure':
return <AzureCredentialsForm {...props} input={input} />;
default:
return null;
}

View file

@ -100,7 +100,6 @@ const getPostureInput = (
enabled: isInputEnabled,
// Merge new vars with existing vars
...(isInputEnabled &&
stream.vars &&
inputVars && {
vars: {
...stream.vars,
@ -183,6 +182,24 @@ export const getCspmCloudFormationDefaultValue = (packageInfo: PackageInfo): str
return cloudFormationTemplate;
};
export const getArmTemplateUrlFromCspmPackage = (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 armTemplateUrl = policyTemplateInputs.reduce((acc, input): string => {
if (!input.vars) return acc;
const template = input.vars.find((v) => v.name === 'arm_template_url')?.default;
return template ? String(template) : acc;
}, '');
return armTemplateUrl;
};
/**
* Input vars that are hidden from the user
*/

View file

@ -0,0 +1,103 @@
/*
* 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 { getAzureArmPropsFromPackagePolicy } from '../../../../../../../services/get_azure_arm_props_from_package_policy';
import { useCreateAzureArmTemplateUrl } from '../../../../../../../hooks/use_create_azure_arm_template_url';
import { AzureArmTemplateGuide } from '../../../../../../../components/azure_arm_template_guide';
import type { AgentPolicy, PackagePolicy } from '../../../../../types';
import { sendGetEnrollmentAPIKeys } from '../../../../../hooks';
export const PostInstallAzureArmTemplateModal: 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 azureArmTemplateProps = getAzureArmPropsFromPackagePolicy(packagePolicy);
const { azureArmTemplateUrl, error, isError, isLoading } = useCreateAzureArmTemplateUrl({
enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key,
azureArmTemplateProps,
});
return (
<EuiModal data-test-subj="postInstallAzureArmTemplateModal" onClose={onCancel}>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="confirmAzureArmTemplateModalTitleText">
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallAzureArmTemplateModalModalTitle"
defaultMessage="ARM Template deployment"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<AzureArmTemplateGuide azureAccountType={azureArmTemplateProps.azureAccountType} />
{error && isError && (
<>
<EuiSpacer size="m" />
<EuiCallOut title={error} color="danger" iconType="error" />
</>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="confirmAzureArmTemplateModalCancelButton"
onClick={onCancel}
>
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallAzureArmTemplateModal.cancelButton"
defaultMessage="Add ARM Template later"
/>
</EuiButtonEmpty>
<EuiButton
data-test-subj="confirmAzureArmTemplateModalConfirmButton"
onClick={() => {
window.open(azureArmTemplateUrl);
onConfirm();
}}
fill
color="primary"
isLoading={isLoading}
isDisabled={isError}
>
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallAzureArmTemplateModalConfirmButtonLabel"
defaultMessage="Launch ARM Template"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -9,6 +9,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { safeLoad } from 'js-yaml';
import { getAzureArmPropsFromPackagePolicy } from '../../../../../../../services/get_azure_arm_props_from_package_policy';
import type {
AgentPolicy,
NewPackagePolicy,
@ -304,12 +306,21 @@ export function useOnSubmit({
force,
});
const hasAzureArmTemplate = data?.item
? getAzureArmPropsFromPackagePolicy(data.item).templateUrl
: false;
const hasCloudFormation = data?.item
? getCloudFormationPropsFromPackagePolicy(data.item).templateUrl
: false;
const hasGoogleCloudShell = data?.item ? getCloudShellUrlFromPackagePolicy(data.item) : false;
if (hasAzureArmTemplate) {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_AZURE_ARM_TEMPLATE');
} else {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
}
if (hasCloudFormation) {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_CLOUD_FORMATION');
} else {
@ -324,6 +335,10 @@ export function useOnSubmit({
setSavedPackagePolicy(data!.item);
const hasAgentsAssigned = agentCount && agentPolicy;
if (!hasAgentsAssigned && hasAzureArmTemplate) {
setFormState('SUBMITTED_AZURE_ARM_TEMPLATE');
return;
}
if (!hasAgentsAssigned && hasCloudFormation) {
setFormState('SUBMITTED_CLOUD_FORMATION');
return;

View file

@ -61,6 +61,7 @@ import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from '.
import { useDevToolsRequest, useOnSubmit } from './hooks';
import { PostInstallCloudFormationModal } from './components/post_install_cloud_formation_modal';
import { PostInstallGoogleCloudShellModal } from './components/post_install_google_cloud_shell_modal';
import { PostInstallAzureArmTemplateModal } from './components/post_install_azure_arm_template_modal';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@ -415,6 +416,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
{formState === 'SUBMITTED_AZURE_ARM_TEMPLATE' && agentPolicy && savedPackagePolicy && (
<PostInstallAzureArmTemplateModal
agentPolicy={agentPolicy}
packagePolicy={savedPackagePolicy}
onConfirm={() => navigateAddAgent(savedPackagePolicy)}
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
{formState === 'SUBMITTED_CLOUD_FORMATION' && agentPolicy && savedPackagePolicy && (
<PostInstallCloudFormationModal
agentPolicy={agentPolicy}

View file

@ -22,6 +22,7 @@ export type PackagePolicyFormState =
| 'LOADING'
| 'SUBMITTED'
| 'SUBMITTED_NO_AGENTS'
| 'SUBMITTED_AZURE_ARM_TEMPLATE'
| 'SUBMITTED_CLOUD_FORMATION'
| 'SUBMITTED_GOOGLE_CLOUD_SHELL';

View file

@ -0,0 +1,73 @@
/*
* 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, EuiSpacer, EuiCallOut, EuiSkeletonText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { AzureArmTemplateGuide } from '../azure_arm_template_guide';
import { useCreateAzureArmTemplateUrl } from '../../hooks/use_create_azure_arm_template_url';
import type { CloudSecurityIntegration } from './types';
interface Props {
enrollmentAPIKey?: string;
cloudSecurityIntegration: CloudSecurityIntegration;
}
export const AzureArmTemplateInstructions: React.FunctionComponent<Props> = ({
enrollmentAPIKey,
cloudSecurityIntegration,
}) => {
const { isLoading, azureArmTemplateUrl, error, isError } = useCreateAzureArmTemplateUrl({
enrollmentAPIKey,
azureArmTemplateProps: cloudSecurityIntegration?.azureArmTemplateProps,
});
if (error && isError) {
return (
<>
<EuiSpacer size="m" />
<EuiCallOut title={error} color="danger" iconType="error" />
</>
);
}
return (
<EuiSkeletonText
lines={3}
size="m"
isLoading={isLoading || cloudSecurityIntegration?.isLoading}
contentAriaLabel={i18n.translate(
'xpack.fleet.agentEnrollment.azureArmTemplate.loadingAriaLabel',
{
defaultMessage: 'Loading ARM Template instructions',
}
)}
>
<AzureArmTemplateGuide
azureAccountType={cloudSecurityIntegration?.azureArmTemplateProps?.azureAccountType}
/>
<EuiSpacer size="m" />
<EuiButton
color="primary"
fill
target="_blank"
iconSide="left"
iconType="launch"
fullWidth
href={azureArmTemplateUrl}
>
<FormattedMessage
id="xpack.fleet.agentEnrollment.azureArmTemplate.launchButton"
defaultMessage="Launch ARM Template"
/>
</EuiButton>
</EuiSkeletonText>
);
};

View file

@ -7,6 +7,10 @@
import { useState, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS } from '../../services/get_template_url_from_package_info';
import { SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG } from '../../services/get_template_url_from_agent_policy';
import type { PackagePolicy, AgentPolicy } from '../../types';
import { sendGetOneAgentPolicy, useGetPackageInfoByKeyQuery, useStartServices } from '../../hooks';
import {
@ -14,18 +18,16 @@ import {
FLEET_CLOUD_SECURITY_POSTURE_PACKAGE,
FLEET_CLOUD_DEFEND_PACKAGE,
} from '../../../common';
import { getCloudShellUrlFromAgentPolicy } from '../../services';
import { getTemplateUrlFromPackageInfo, getCloudShellUrlFromAgentPolicy } from '../../services';
import {
getCloudFormationTemplateUrlFromPackageInfo,
getCloudFormationTemplateUrlFromAgentPolicy,
} from '../../services';
import { getTemplateUrlFromAgentPolicy } from '../../services';
import type {
K8sMode,
CloudSecurityIntegrationType,
CloudSecurityIntegrationAwsAccountType,
CloudSecurityIntegration,
CloudSecurityIntegrationAzureAccountType,
} from './types';
// Packages that requires custom elastic-agent manifest
@ -99,6 +101,9 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) {
{ enabled: Boolean(cloudSecurityPackagePolicy) }
);
const AWS_ACCOUNT_TYPE = 'aws.account_type';
const AZURE_ACCOUNT_TYPE = 'azure.account_type';
const cloudSecurityIntegration: CloudSecurityIntegration | undefined = useMemo(() => {
if (!agentPolicy || !cloudSecurityPackagePolicy) {
return undefined;
@ -109,8 +114,15 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) {
if (!integrationType) return undefined;
const cloudFormationTemplateFromAgentPolicy =
getCloudFormationTemplateUrlFromAgentPolicy(agentPolicy);
const cloudFormationTemplateFromAgentPolicy = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.CLOUD_FORMATION,
agentPolicy
);
const azureArmTemplateFromAgentPolicy = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.ARM_TEMPLATE,
agentPolicy
);
// Use the latest CloudFormation template for the current version
// So it guarantee that the template version matches the integration version
@ -118,16 +130,31 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) {
// In case it can't find the template for the current version,
// it will fallback to the one from the agent policy.
const cloudFormationTemplateUrl = packageInfoData?.item
? getCloudFormationTemplateUrlFromPackageInfo(packageInfoData.item, integrationType)
? getTemplateUrlFromPackageInfo(
packageInfoData.item,
integrationType,
SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS.CLOUD_FORMATION
)
: cloudFormationTemplateFromAgentPolicy;
const AWS_ACCOUNT_TYPE = 'aws.account_type';
const cloudFormationAwsAccountType: CloudSecurityIntegrationAwsAccountType | undefined =
cloudSecurityPackagePolicy?.inputs?.find((input) => input.enabled)?.streams?.[0]?.vars?.[
AWS_ACCOUNT_TYPE
]?.value;
const azureArmTemplateUrl = packageInfoData?.item
? getTemplateUrlFromPackageInfo(
packageInfoData.item,
integrationType,
SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS.ARM_TEMPLATE
)
: azureArmTemplateFromAgentPolicy;
const azureArmTemplateAccountType: CloudSecurityIntegrationAzureAccountType | undefined =
cloudSecurityPackagePolicy?.inputs?.find((input) => input.enabled)?.streams?.[0]?.vars?.[
AZURE_ACCOUNT_TYPE
]?.value;
const cloudShellUrl = getCloudShellUrlFromAgentPolicy(agentPolicy);
return {
isLoading,
@ -137,6 +164,11 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) {
awsAccountType: cloudFormationAwsAccountType,
templateUrl: cloudFormationTemplateUrl,
},
isAzureArmTemplate: Boolean(azureArmTemplateFromAgentPolicy),
azureArmTemplateProps: {
azureAccountType: azureArmTemplateAccountType,
templateUrl: azureArmTemplateUrl,
},
cloudShellUrl,
};
}, [agentPolicy, packageInfoData?.item, isLoading, cloudSecurityPackagePolicy]);

View file

@ -82,6 +82,7 @@ export const Instructions = (props: InstructionProps) => {
useEffect(() => {
// If we detect a CloudFormation integration, we want to hide the selection type
if (
props.cloudSecurityIntegration?.isAzureArmTemplate ||
props.cloudSecurityIntegration?.isCloudFormation ||
props.cloudSecurityIntegration?.cloudShellUrl
) {

View file

@ -38,6 +38,7 @@ import {
InstallManagedAgentStep,
InstallCloudFormationManagedAgentStep,
InstallGoogleCloudShellManagedAgentStep,
InstallAzureArmTemplateManagedAgentStep,
IncomingDataConfirmationStep,
} from '.';
@ -274,6 +275,15 @@ export const ManagedSteps: React.FunctionComponent<InstructionProps> = ({
cloudShellCommand: installManagedCommands.googleCloudShell,
})
);
} else if (cloudSecurityIntegration?.isAzureArmTemplate) {
steps.push(
InstallAzureArmTemplateManagedAgentStep({
selectedApiKeyId,
apiKeyData,
enrollToken,
cloudSecurityIntegration,
})
);
} else {
steps.push(
InstallManagedAgentStep({

View file

@ -10,6 +10,7 @@ export * from './agent_enrollment_key_selection_step';
export * from './agent_policy_selection_step';
export * from './configure_standalone_agent_step';
export * from './incoming_data_confirmation_step';
export * from './install_azure_arm_template_managed_agent_step';
export * from './install_cloud_formation_managed_agent_step';
export * from './install_google_cloud_shell_managed_agent_step';
export * from './install_managed_agent_step';

View file

@ -0,0 +1,52 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
import { AzureArmTemplateInstructions } from '../azure_arm_template_instructions';
import type { GetOneEnrollmentAPIKeyResponse } from '../../../../common/types/rest_spec/enrollment_api_key';
import type { CloudSecurityIntegration } from '../types';
export const InstallAzureArmTemplateManagedAgentStep = ({
selectedApiKeyId,
apiKeyData,
enrollToken,
isComplete,
cloudSecurityIntegration,
}: {
selectedApiKeyId?: string;
apiKeyData?: GetOneEnrollmentAPIKeyResponse | null;
enrollToken?: string;
isComplete?: boolean;
cloudSecurityIntegration?: CloudSecurityIntegration | undefined;
}): EuiContainedStepProps => {
const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled';
const status = isComplete ? 'complete' : nonCompleteStatus;
return {
status,
title: i18n.translate(
'xpack.fleet.agentEnrollment.azureArmTemplate.stepEnrollAndRunAgentTitle',
{ defaultMessage: 'Install Elastic Agent on your cloud' }
),
children:
selectedApiKeyId && apiKeyData && cloudSecurityIntegration ? (
<AzureArmTemplateInstructions
cloudSecurityIntegration={cloudSecurityIntegration}
enrollmentAPIKey={enrollToken}
/>
) : (
<React.Fragment />
),
};
};

View file

@ -17,6 +17,9 @@ export type K8sMode =
export type CloudSecurityIntegrationType = 'kspm' | 'vuln_mgmt' | 'cspm';
export type CloudSecurityIntegrationAwsAccountType = 'single-account' | 'organization-account';
export type CloudSecurityIntegrationAzureAccountType =
| 'single-account-azure'
| 'organization-account-azure';
export type FlyoutMode = 'managed' | 'standalone';
export type SelectionType = 'tabs' | 'radio' | undefined;
@ -26,11 +29,18 @@ export interface CloudFormationProps {
awsAccountType: CloudSecurityIntegrationAwsAccountType | undefined;
}
export interface AzureArmTemplateProps {
templateUrl: string | undefined;
azureAccountType: CloudSecurityIntegrationAzureAccountType | undefined;
}
export interface CloudSecurityIntegration {
integrationType: CloudSecurityIntegrationType | undefined;
isLoading: boolean;
isCloudFormation: boolean;
isAzureArmTemplate: boolean;
cloudFormationProps?: CloudFormationProps;
azureArmTemplateProps?: AzureArmTemplateProps;
cloudShellUrl: string | undefined;
}

View file

@ -0,0 +1,72 @@
/*
* 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';
import type { CloudSecurityIntegrationAzureAccountType } from './agent_enrollment_flyout/types';
const azureResourceManagerLink =
'https://azure.microsoft.com/en-us/get-started/azure-portal/resource-manager';
export const AzureArmTemplateGuide = ({
azureAccountType,
}: {
azureAccountType?: CloudSecurityIntegrationAzureAccountType;
}) => {
return (
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.azureArmTemplate.guide.description"
defaultMessage="An Azure Resource Manager (ARM) Template will create all the necessary resources to evaluate the security posture of your Azure organization. Follow the steps below to launch the ARM template. Learn more about {learnMore}."
values={{
learnMore: (
<EuiLink
href={azureResourceManagerLink}
target="_blank"
rel="noopener nofollow noreferrer"
data-test-subj="azure-resource-manager-link"
>
<FormattedMessage
id="xpack.fleet.azureArmTemplate.guide.learnMoreLinkText"
defaultMessage="Azure Resource Manager"
/>
</EuiLink>
),
}}
/>
</p>
<EuiText size="s" color="subdued">
<ol>
{azureAccountType === 'organization-account-azure' ? (
<li>
<FormattedMessage
id="xpack.fleet.azureArmTemplate.guide.steps.organizationLogin"
defaultMessage="Log into your Azure Portal"
/>
</li>
) : (
<li>
<FormattedMessage
id="xpack.fleet.azureArmTemplate.guide.steps.login"
defaultMessage="Log into your Azure Portal"
/>
</li>
)}
<li>
<FormattedMessage
id="xpack.fleet.azureArmTemplate.guide.steps.launch"
defaultMessage="Click the Launch ARM Template button below."
/>
</li>
</ol>
</EuiText>
</EuiText>
);
};

View file

@ -0,0 +1,51 @@
/*
* 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 type { AzureArmTemplateProps } from '../components/agent_enrollment_flyout/types';
import { useGetSettings } from './use_request';
export const useCreateAzureArmTemplateUrl = ({
enrollmentAPIKey,
azureArmTemplateProps,
}: {
enrollmentAPIKey: string | undefined;
azureArmTemplateProps: AzureArmTemplateProps | undefined;
}) => {
const { data, isLoading } = useGetSettings();
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 azureArmTemplateUrl = azureArmTemplateProps?.templateUrl;
return {
isLoading,
azureArmTemplateUrl,
isError,
error,
};
};

View file

@ -0,0 +1,32 @@
/*
* 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 { CloudSecurityIntegrationAzureAccountType } from '../components/agent_enrollment_flyout/types';
import type { PackagePolicy } from '../types';
import type { AzureArmTemplateProps } from '../components/agent_enrollment_flyout/types';
const AZURE_ACCOUNT_TYPE = 'azure.account_type';
/**
* Get the Azure Arm Template url from a package policy
* It looks for a config with an arm_template_url object present in the enabled inputs of the package policy
*/
export const getAzureArmPropsFromPackagePolicy = (
packagePolicy?: PackagePolicy
): AzureArmTemplateProps => {
const templateUrl: CloudSecurityIntegrationAzureAccountType | undefined =
packagePolicy?.inputs?.find((input) => input.enabled)?.config?.arm_template_url?.value;
const azureAccountType: CloudSecurityIntegrationAzureAccountType | undefined =
packagePolicy?.inputs?.find((input) => input.enabled)?.streams?.[0]?.vars?.[AZURE_ACCOUNT_TYPE]
?.value;
return {
templateUrl,
azureAccountType,
};
};

View file

@ -1,66 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getCloudFormationTemplateUrlFromPackageInfo } from './get_cloud_formation_template_url_from_package_info';
describe('getCloudFormationTemplateUrlFromPackageInfo', () => {
test('returns undefined when packageInfo is undefined', () => {
const result = getCloudFormationTemplateUrlFromPackageInfo(undefined, 'test');
expect(result).toBeUndefined();
});
test('returns undefined when packageInfo has no policy_templates', () => {
const packageInfo = { inputs: [] };
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'test');
expect(result).toBeUndefined();
});
test('returns undefined when integrationType is not found in policy_templates', () => {
const packageInfo = { policy_templates: [{ name: 'template1' }, { name: 'template2' }] };
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'nonExistentTemplate');
expect(result).toBeUndefined();
});
test('returns undefined when no input in the policy template has a cloudFormationTemplate', () => {
const packageInfo = {
policy_templates: [
{
name: 'template1',
inputs: [
{ name: 'input1', vars: [] },
{ name: 'input2', vars: [{ name: 'var1', default: 'value1' }] },
],
},
],
};
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'template1');
expect(result).toBeUndefined();
});
test('returns the cloudFormationTemplate from the policy template', () => {
const packageInfo = {
policy_templates: [
{
name: 'template1',
inputs: [
{ name: 'input1', vars: [] },
{
name: 'input2',
vars: [{ name: 'cloud_formation_template', default: 'cloud_formation_template_url' }],
},
],
},
],
};
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'template1');
expect(result).toBe('cloud_formation_template_url');
});
});

View file

@ -4,18 +4,26 @@
* 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';
import {
getTemplateUrlFromAgentPolicy,
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG,
} from './get_template_url_from_agent_policy';
describe('getCloudFormationTemplateUrlFromAgentPolicy', () => {
describe('getTemplateUrlFromAgentPolicy', () => {
it('should return undefined when selectedPolicy is undefined', () => {
const result = getCloudFormationTemplateUrlFromAgentPolicy();
const result = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.CLOUD_FORMATION
);
expect(result).toBeUndefined();
});
it('should return undefined when selectedPolicy has no package_policies', () => {
const selectedPolicy = {};
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromAgentPolicy(selectedPolicy);
const result = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.CLOUD_FORMATION,
// @ts-expect-error
selectedPolicy
);
expect(result).toBeUndefined();
});
@ -37,8 +45,11 @@ describe('getCloudFormationTemplateUrlFromAgentPolicy', () => {
},
],
};
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromAgentPolicy(selectedPolicy);
const result = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.CLOUD_FORMATION,
// @ts-expect-error
selectedPolicy
);
expect(result).toBeUndefined();
});
@ -61,8 +72,38 @@ describe('getCloudFormationTemplateUrlFromAgentPolicy', () => {
},
],
};
// @ts-expect-error
const result = getCloudFormationTemplateUrlFromAgentPolicy(selectedPolicy);
const result = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.CLOUD_FORMATION,
// @ts-expect-error
selectedPolicy
);
expect(result).toBe('url3');
});
it('should return the first config.arm_template_url when available', () => {
const selectedPolicy = {
package_policies: [
{
inputs: [
{ enabled: false, config: { arm_template_url: { value: 'url1' } } },
{ enabled: false, config: { arm_template_url: { value: 'url2' } } },
{ enabled: false, config: { other_property: 'value' } },
],
},
{
inputs: [
{ enabled: false, config: {} },
{ enabled: true, config: { arm_template_url: { value: 'url3' } } },
{ enabled: true, config: { arm_template_url: { value: 'url4' } } },
],
},
],
};
const result = getTemplateUrlFromAgentPolicy(
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG.ARM_TEMPLATE,
// @ts-expect-error
selectedPolicy
);
expect(result).toBe('url3');
});
});

View file

@ -7,12 +7,15 @@
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) => {
export const SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG = {
CLOUD_FORMATION: 'cloud_formation_template_url',
ARM_TEMPLATE: 'arm_template_url',
};
export const getTemplateUrlFromAgentPolicy = (
templateUrlFieldName: string,
selectedPolicy?: AgentPolicy
) => {
const cloudFormationTemplateUrl = selectedPolicy?.package_policies?.reduce(
(acc, packagePolicy) => {
const findCloudFormationTemplateUrlConfig = packagePolicy.inputs?.reduce(
@ -20,8 +23,8 @@ export const getCloudFormationTemplateUrlFromAgentPolicy = (selectedPolicy?: Age
if (accInput !== '') {
return accInput;
}
if (input?.enabled && input?.config?.cloud_formation_template_url) {
return input.config.cloud_formation_template_url.value;
if (input?.enabled && input?.config?.[templateUrlFieldName]) {
return input.config[templateUrlFieldName].value;
}
return accInput;
},

View file

@ -0,0 +1,112 @@
/*
* 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 { PackageInfo } from '../types';
import {
getTemplateUrlFromPackageInfo,
SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS,
} from './get_template_url_from_package_info';
describe('getTemplateUrlFromPackageInfo', () => {
test('returns undefined when packageInfo is undefined', () => {
const result = getTemplateUrlFromPackageInfo(undefined, 'test', 'cloud_formation_template_url');
expect(result).toBeUndefined();
});
test('returns undefined when packageInfo has no policy_templates', () => {
const packageInfo = { inputs: [] } as unknown as PackageInfo;
const result = getTemplateUrlFromPackageInfo(
packageInfo,
'test',
'cloud_formation_template_url'
);
expect(result).toBeUndefined();
});
test('returns undefined when integrationType is not found in policy_templates', () => {
const packageInfo = {
policy_templates: [{ name: 'template1' }, { name: 'template2' }],
} as PackageInfo;
const result = getTemplateUrlFromPackageInfo(
packageInfo,
'nonExistentTemplate',
'cloud_formation_template_url'
);
expect(result).toBeUndefined();
});
test('returns undefined when no input in the policy template has a cloudFormationTemplate', () => {
const packageInfo = {
policy_templates: [
{
name: 'template1',
inputs: [
{ name: 'input1', vars: [] },
{ name: 'input2', vars: [{ name: 'var1', default: 'value1' }] },
],
},
],
} as unknown as PackageInfo;
const result = getTemplateUrlFromPackageInfo(
packageInfo,
'template1',
'cloud_formation_template_url'
);
expect(result).toBeUndefined();
});
test('returns the cloudFormationTemplate from the policy template', () => {
const packageInfo = {
policy_templates: [
{
name: 'template1',
inputs: [
{ name: 'input1', vars: [] },
{
name: 'input2',
vars: [
{
name: SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS.CLOUD_FORMATION,
default: 'cloud_formation_template_url',
},
],
},
],
},
],
} as unknown as PackageInfo;
const result = getTemplateUrlFromPackageInfo(
packageInfo,
'template1',
SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS.CLOUD_FORMATION
);
expect(result).toBe('cloud_formation_template_url');
});
test('returns the armTemplateUrl from the policy template', () => {
const packageInfo = {
policy_templates: [
{
name: 'template1',
inputs: [
{ name: 'input1', vars: [] },
{
name: 'input2',
vars: [{ name: 'arm_template_url', default: 'arm_template_url_value' }],
},
],
},
],
} as unknown as PackageInfo;
const result = getTemplateUrlFromPackageInfo(packageInfo, 'template1', 'arm_template_url');
expect(result).toBe('arm_template_url_value');
});
});

View file

@ -7,14 +7,15 @@
import type { PackageInfo } from '../types';
/**
* Get the cloud formation template url from the PackageInfo
* It looks for a input var with a object containing cloud_formation_template_url present in
* the package_policies inputs of the given integration type
*/
export const getCloudFormationTemplateUrlFromPackageInfo = (
export const SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS = {
CLOUD_FORMATION: 'cloud_formation_template',
ARM_TEMPLATE: 'arm_template_url',
};
export const getTemplateUrlFromPackageInfo = (
packageInfo: PackageInfo | undefined,
integrationType: string
integrationType: string,
templateUrlFieldName: string
): string | undefined => {
if (!packageInfo?.policy_templates) return undefined;
@ -24,7 +25,7 @@ export const getCloudFormationTemplateUrlFromPackageInfo = (
if ('inputs' in policyTemplate) {
const cloudFormationTemplate = policyTemplate.inputs?.reduce((acc, input): string => {
if (!input.vars) return acc;
const template = input.vars.find((v) => v.name === 'cloud_formation_template')?.default;
const template = input.vars.find((v) => v.name === templateUrlFieldName)?.default;
return template ? String(template) : acc;
}, '');
return cloudFormationTemplate !== '' ? cloudFormationTemplate : undefined;

View file

@ -49,7 +49,7 @@ export { pkgKeyFromPackageInfo } from './pkg_key_from_package_info';
export { createExtensionRegistrationCallback } from './ui_extensions';
export { incrementPolicyName } from './increment_policy_name';
export { getCloudFormationPropsFromPackagePolicy } from './get_cloud_formation_props_from_package_policy';
export { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy';
export { getCloudFormationTemplateUrlFromPackageInfo } from './get_cloud_formation_template_url_from_package_info';
export { getTemplateUrlFromAgentPolicy } from './get_template_url_from_agent_policy';
export { getTemplateUrlFromPackageInfo } from './get_template_url_from_package_info';
export { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy';
export { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy';

View file

@ -11359,7 +11359,6 @@
"xpack.csp.cspmIntegration.awsOption.nameTitle": "Amazon Web Services",
"xpack.csp.cspmIntegration.azureOption.benchmarkTitle": "CIS Azure",
"xpack.csp.cspmIntegration.azureOption.nameTitle": "Azure",
"xpack.csp.cspmIntegration.azureOption.tooltipContent": "Bientôt disponible",
"xpack.csp.cspmIntegration.gcpOption.benchmarkTitle": "CIS GCP",
"xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP",
"xpack.csp.cspmIntegration.integration.nameTitle": "Gestion du niveau de sécurité du cloud",

View file

@ -11374,7 +11374,6 @@
"xpack.csp.cspmIntegration.awsOption.nameTitle": "Amazon Web Services",
"xpack.csp.cspmIntegration.azureOption.benchmarkTitle": "CIS Azure",
"xpack.csp.cspmIntegration.azureOption.nameTitle": "Azure",
"xpack.csp.cspmIntegration.azureOption.tooltipContent": "まもなくリリース",
"xpack.csp.cspmIntegration.gcpOption.benchmarkTitle": "CIS GCP",
"xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP",
"xpack.csp.cspmIntegration.integration.nameTitle": "クラウドセキュリティ態勢管理",

View file

@ -11374,7 +11374,6 @@
"xpack.csp.cspmIntegration.awsOption.nameTitle": "Amazon Web Services",
"xpack.csp.cspmIntegration.azureOption.benchmarkTitle": "CIS Azure",
"xpack.csp.cspmIntegration.azureOption.nameTitle": "Azure",
"xpack.csp.cspmIntegration.azureOption.tooltipContent": "即将推出",
"xpack.csp.cspmIntegration.gcpOption.benchmarkTitle": "CIS GCP",
"xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP",
"xpack.csp.cspmIntegration.integration.nameTitle": "云安全态势管理",