diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 7f0b4f62fb216..23de4b6d02e36 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -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: [], +}; diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index 036826ba9e6de..092129a6762e2 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -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'; diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts index 1cf006589bd7a..85815e780b062 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts @@ -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) { diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 4a5d5bfc7bd9a..eb6318e7c6727 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -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', - }), }, ], }, diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx new file mode 100644 index 0000000000000..51ef9c7bcb955 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx @@ -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 ( + <> + + +

+ +

+
+ + + + + + ), + }} + /> + + + ); +}; + +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; + 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 ( + + + + ); + } + + return ( + <> + +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
+
+ + + + {i18n.translate('xpack.csp.azureIntegration.documentationLinkText', { + defaultMessage: 'documentation', + })} + + ), + }} + /> + + + ); +}; + +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 ( + <> + + + + + + ); + } + + return ( + <> + + + + idSelected !== setupFormat && onSetupFormatChange(idSelected) + } + /> + + {setupFormat === AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE && ( + + )} + + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx new file mode 100644 index 0000000000000..5a074ad4c4ebc --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx @@ -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; + +export interface AzureOptionValue { + label: string; + info: React.ReactNode; + fields: AzureCredentialsFields; +} + +export type AzureOptions = Record; + +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: {}, + }, +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/hooks.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/hooks.ts new file mode 100644 index 0000000000000..011c39cf8038e --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/hooks.ts @@ -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 +): 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; + 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(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, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx index c9ba38eccb2e6..9a50658a6a1f3 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx @@ -17,7 +17,7 @@ export interface CspRadioGroupProps { size?: 's' | 'm'; } -interface CspRadioOption { +export interface CspRadioOption { disabled?: boolean; id: string; label: string; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts index 2f2d44895db36..c04114fc83487 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts @@ -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, diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index c5d4c0e8de3e2..5cb50fd49bc7b 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -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('', () => { expect(option3).toBeInTheDocument(); expect(option1).toBeEnabled(); expect(option2).toBeEnabled(); - expect(option3).toBeDisabled(); + expect(option3).toBeEnabled(); expect(option1).toBeChecked(); }); @@ -1130,4 +1137,44 @@ describe('', () => { }); }); }); + + 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( + + ); + + 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(); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); + }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 9ee0a7ac18f76..67a617decac76 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -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 ): AwsAccountType | undefined => input.streams[0].vars?.['aws.account_type']?.value; @@ -175,7 +202,7 @@ const AwsAccountTypeSelect = ({ @@ -277,6 +304,89 @@ const GcpAccountTypeSelect = ({ ); }; +const getAzureAccountType = ( + input: Extract +): AzureAccountType | undefined => input.streams[0].vars?.['azure.account_type']?.value; + +const AzureAccountTypeSelect = ({ + input, + newPolicy, + updatePolicy, +}: { + input: Extract; + 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 ( + <> + + + + + { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'azure.account_type': { + value: accountType, + type: 'text', + }, + }) + ); + }} + size="m" + /> + {getAzureAccountType(input) === AZURE_ORGANIZATION_ACCOUNT && ( + <> + + + + + + )} + {getAzureAccountType(input) === AZURE_SINGLE_ACCOUNT && ( + <> + + + + + + )} + + ); +}; + const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) => (
{fields.map(({ value, id, label, error }) => ( @@ -303,7 +413,9 @@ export const CspPolicyTemplateForm = memo onChange({ isValid, updatedPolicy }), + (updatedPolicy: NewPackagePolicy) => { + onChange({ isValid, updatedPolicy }); + }, [onChange, isValid] ); /** @@ -434,13 +546,6 @@ export const CspPolicyTemplateForm = memo - {/* Defines the name/description */} - updatePolicy({ ...newPolicy, [field]: value })} - /> - - {/* AWS account type selection box */} {input.type === 'cloudbeat/cis_aws' && ( )} + {input.type === 'cloudbeat/cis_azure' && ( + + )} + + {/* Defines the name/description */} + + updatePolicy({ ...newPolicy, [field]: value })} + /> + {/* Defines the vars of the enabled input of the active policy template */} ; case 'cloudbeat/cis_gcp': return ; + case 'cloudbeat/cis_azure': + return ; default: return null; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts index 7d4233b8016df..9db4c9bf22a75 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts @@ -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 */ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx new file mode 100644 index 0000000000000..747a894a647fe --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx @@ -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 ( + + + + + + + + + + {error && isError && ( + <> + + + + )} + + + + + + + { + window.open(azureArmTemplateUrl); + onConfirm(); + }} + fill + color="primary" + isLoading={isLoading} + isDisabled={isError} + > + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index da4ecfbe3569a..f76ef340a2c98 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -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; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index e7a35ae48dbda..375a19b41cdc0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -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 && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} {formState === 'SUBMITTED_CLOUD_FORMATION' && agentPolicy && savedPackagePolicy && ( = ({ + enrollmentAPIKey, + cloudSecurityIntegration, +}) => { + const { isLoading, azureArmTemplateUrl, error, isError } = useCreateAzureArmTemplateUrl({ + enrollmentAPIKey, + azureArmTemplateProps: cloudSecurityIntegration?.azureArmTemplateProps, + }); + + if (error && isError) { + return ( + <> + + + + ); + } + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx index 9345e4e3a6663..bc541580ddce7 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx @@ -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]); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx index 0a413856e4f34..39d4ab5e0428d 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx @@ -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 ) { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx index 12a0025efd46d..2cf22d7a26dbb 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx @@ -38,6 +38,7 @@ import { InstallManagedAgentStep, InstallCloudFormationManagedAgentStep, InstallGoogleCloudShellManagedAgentStep, + InstallAzureArmTemplateManagedAgentStep, IncomingDataConfirmationStep, } from '.'; @@ -274,6 +275,15 @@ export const ManagedSteps: React.FunctionComponent = ({ cloudShellCommand: installManagedCommands.googleCloudShell, }) ); + } else if (cloudSecurityIntegration?.isAzureArmTemplate) { + steps.push( + InstallAzureArmTemplateManagedAgentStep({ + selectedApiKeyId, + apiKeyData, + enrollToken, + cloudSecurityIntegration, + }) + ); } else { steps.push( InstallManagedAgentStep({ diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/index.tsx index 8c9adb376f423..a068cd8de6af5 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/index.tsx @@ -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'; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx new file mode 100644 index 0000000000000..a24e958b38916 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx @@ -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 ? ( + + ) : ( + + ), + }; +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index e292182a3cd92..abad38a0d74ae 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -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; } diff --git a/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx b/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx new file mode 100644 index 0000000000000..7cfb6542a02fe --- /dev/null +++ b/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx @@ -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 ( + +

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

+ +
    + {azureAccountType === 'organization-account-azure' ? ( +
  1. + +
  2. + ) : ( +
  3. + +
  4. + )} +
  5. + +
  6. +
+
+
+ ); +}; diff --git a/x-pack/plugins/fleet/public/hooks/use_create_azure_arm_template_url.ts b/x-pack/plugins/fleet/public/hooks/use_create_azure_arm_template_url.ts new file mode 100644 index 0000000000000..aed03272c470e --- /dev/null +++ b/x-pack/plugins/fleet/public/hooks/use_create_azure_arm_template_url.ts @@ -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, + }; +}; diff --git a/x-pack/plugins/fleet/public/services/get_azure_arm_props_from_package_policy.ts b/x-pack/plugins/fleet/public/services/get_azure_arm_props_from_package_policy.ts new file mode 100644 index 0000000000000..f064db2e6fadf --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_azure_arm_props_from_package_policy.ts @@ -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, + }; +}; diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.test.ts deleted file mode 100644 index 8ed2fb3ae389a..0000000000000 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts b/x-pack/plugins/fleet/public/services/get_template_url_from_agent_policy.test.ts similarity index 51% rename from x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts rename to x-pack/plugins/fleet/public/services/get_template_url_from_agent_policy.test.ts index 6b4214044f2a0..279f64f412d90 100644 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.test.ts +++ b/x-pack/plugins/fleet/public/services/get_template_url_from_agent_policy.test.ts @@ -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'); }); }); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts b/x-pack/plugins/fleet/public/services/get_template_url_from_agent_policy.ts similarity index 66% rename from x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts rename to x-pack/plugins/fleet/public/services/get_template_url_from_agent_policy.ts index 81aaf5b3fd970..203d267ecc6fa 100644 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_agent_policy.ts +++ b/x-pack/plugins/fleet/public/services/get_template_url_from_agent_policy.ts @@ -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; }, diff --git a/x-pack/plugins/fleet/public/services/get_template_url_from_package_info.test.ts b/x-pack/plugins/fleet/public/services/get_template_url_from_package_info.test.ts new file mode 100644 index 0000000000000..f2cb8e6987ea7 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_template_url_from_package_info.test.ts @@ -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'); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.ts b/x-pack/plugins/fleet/public/services/get_template_url_from_package_info.ts similarity index 68% rename from x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.ts rename to x-pack/plugins/fleet/public/services/get_template_url_from_package_info.ts index 4f5381ccedb3f..cfb7b747d0427 100644 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.ts +++ b/x-pack/plugins/fleet/public/services/get_template_url_from_package_info.ts @@ -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; diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index a98d4126d52f3..64009e4a11061 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -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'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0b350d50056b6..94581a34d502b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11362,7 +11362,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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c6948a329c948..8a2fea5e78141 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11377,7 +11377,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": "クラウドセキュリティ態勢管理", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 491e2870c636d..5171d80a6a540 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11377,7 +11377,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": "云安全态势管理",