diff --git a/x-pack/platform/plugins/shared/fleet/common/services/simplified_package_policy_helper.ts b/x-pack/platform/plugins/shared/fleet/common/services/simplified_package_policy_helper.ts index b3bb697ebcc5a..525494d5b53bd 100644 --- a/x-pack/platform/plugins/shared/fleet/common/services/simplified_package_policy_helper.ts +++ b/x-pack/platform/plugins/shared/fleet/common/services/simplified_package_policy_helper.ts @@ -46,6 +46,7 @@ export interface SimplifiedPackagePolicy { policy_id?: string | null; policy_ids: string[]; output_id?: string; + cloud_connector_id?: string | null; namespace: string; name: string; description?: string; @@ -172,6 +173,7 @@ export function simplifiedPackagePolicytoNewPackagePolicy( vars: packageLevelVars, supports_agentless: supportsAgentless, supports_cloud_connector: supportsCloudConnector, + cloud_connector_id: cloudConnectorId, additional_datastreams_permissions: additionalDatastreamsPermissions, } = data; const packagePolicy = { @@ -184,6 +186,7 @@ export function simplifiedPackagePolicytoNewPackagePolicy( ), supports_agentless: supportsAgentless, supports_cloud_connector: supportsCloudConnector, + cloud_connector_id: cloudConnectorId, output_id: outputId, }; diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 02fedcd5b3ef1..55ec516a5602a 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -140,16 +140,15 @@ async function savePackagePolicy(pkgPolicy: CreatePackagePolicyRequest['body']) export const updateAgentlessCloudConnectorConfig = ( packagePolicy: NewPackagePolicy, newAgentPolicy: NewAgentPolicy, - setNewAgentPolicy: (policy: NewAgentPolicy) => void, - setPackagePolicy: (policy: NewPackagePolicy) => void + setNewAgentPolicy: (policy: NewAgentPolicy) => void ) => { const input = packagePolicy.inputs?.filter( (pinput: NewPackagePolicyInput) => pinput.enabled === true )[0]; - const enabled = input?.streams?.[0]?.vars?.['aws.supports_cloud_connectors']?.value; if ( - newAgentPolicy.agentless?.cloud_connectors?.enabled !== enabled && + newAgentPolicy.agentless?.cloud_connectors?.enabled !== + packagePolicy.supports_cloud_connector && newAgentPolicy?.supports_agentless ) { let targetCsp; @@ -171,30 +170,22 @@ export const updateAgentlessCloudConnectorConfig = ( cloud_connectors: undefined, }, }); - - setPackagePolicy({ - ...packagePolicy, - supports_cloud_connector: false, - }); return; } - if (newAgentPolicy.agentless?.cloud_connectors?.enabled !== enabled) { + if ( + newAgentPolicy.agentless?.cloud_connectors?.enabled !== packagePolicy.supports_cloud_connector + ) { setNewAgentPolicy({ ...newAgentPolicy, agentless: { ...newAgentPolicy.agentless, cloud_connectors: { - enabled, + enabled: packagePolicy.supports_cloud_connector ?? false, target_csp: targetCsp, }, }, }); - - setPackagePolicy({ - ...packagePolicy, - supports_cloud_connector: true, - }); } } }; @@ -441,12 +432,7 @@ export function useOnSubmit({ } }, [newInputs, prevSetupTechnology, selectedSetupTechnology, updatePackagePolicy, packagePolicy]); - updateAgentlessCloudConnectorConfig( - packagePolicy, - newAgentPolicy, - setNewAgentPolicy, - setPackagePolicy - ); + updateAgentlessCloudConnectorConfig(packagePolicy, newAgentPolicy, setNewAgentPolicy); const onSaveNavigate = useOnSaveNavigate({ queryParamsPolicyId, diff --git a/x-pack/platform/plugins/shared/fleet/public/constants/index.ts b/x-pack/platform/plugins/shared/fleet/public/constants/index.ts index 0f4481cc4be96..f72ef1bf9ab18 100644 --- a/x-pack/platform/plugins/shared/fleet/public/constants/index.ts +++ b/x-pack/platform/plugins/shared/fleet/public/constants/index.ts @@ -12,6 +12,7 @@ export { INTEGRATIONS_PLUGIN_ID, EPM_API_ROUTES, AGENT_API_ROUTES, + CLOUD_CONNECTOR_API_ROUTES, SO_SEARCH_LIMIT, AGENT_POLICY_SAVED_OBJECT_TYPE, LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, diff --git a/x-pack/platform/plugins/shared/fleet/public/index.ts b/x-pack/platform/plugins/shared/fleet/public/index.ts index 0fbf68cf3ba68..6071b175b6947 100644 --- a/x-pack/platform/plugins/shared/fleet/public/index.ts +++ b/x-pack/platform/plugins/shared/fleet/public/index.ts @@ -21,6 +21,17 @@ export const plugin = (initializerContext: PluginInitializerContext) => { }; export type { NewPackagePolicy, KibanaSavedObjectType } from './types'; +export type { + CloudConnectorSO, + CloudConnectorListOptions, + CreateCloudConnectorRequest, + CloudConnectorVars, + CloudConnectorVarsRecord, + CloudProvider, + CloudConnectorSecretVar, + CloudConnectorSecretVarValue, + AwsCloudConnectorVars, +} from './types'; export { SetupTechnology } from './types'; export { SetupTechnologySelector, @@ -61,7 +72,7 @@ export type { UIExtensionsStorage, } from './types/ui_extensions'; -export { pagePathGetters, EPM_API_ROUTES } from './constants'; +export { pagePathGetters, EPM_API_ROUTES, CLOUD_CONNECTOR_API_ROUTES } from './constants'; export { pkgKeyFromPackageInfo } from './services'; export type { CustomAssetsAccordionProps } from './components/custom_assets_accordion'; export { CustomAssetsAccordion } from './components/custom_assets_accordion'; diff --git a/x-pack/platform/plugins/shared/fleet/server/errors/index.ts b/x-pack/platform/plugins/shared/fleet/server/errors/index.ts index db27f7b3b1be5..6254a764a5818 100644 --- a/x-pack/platform/plugins/shared/fleet/server/errors/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/errors/index.ts @@ -128,6 +128,12 @@ export class CloudConnectorInvalidVarsError extends FleetError { } } +export class CloudConnectorUpdateError extends FleetError { + constructor(message: string) { + super(`Error updating cloud connector in Fleet, ${message}`); + } +} + export class AgentPolicyNameExistsError extends AgentPolicyError {} export class AgentReassignmentError extends FleetError {} export class PackagePolicyIneligibleForUpgradeError extends FleetError {} diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/handlers.ts b/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/handlers.ts index 085dcdbeb5a4a..b2580bb5bb857 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/handlers.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/handlers.ts @@ -76,3 +76,42 @@ export const getCloudConnectorsHandler: FleetRequestHandler< }); } }; + +export interface UpdateCloudConnectorRequest { + name?: string; + vars?: CloudConnectorVars; + cloudProvider?: CloudProvider; + packagePolicyCount?: number; + updated_at?: string; +} + +export const updateCloudConnectorHandler: FleetRequestHandler< + { id: string }, + undefined, + UpdateCloudConnectorRequest +> = async (context, request, response) => { + const fleetContext = await context.fleet; + const { internalSoClient } = fleetContext; + const logger = appContextService + .getLogger() + .get('CloudConnectorService updateCloudConnectorHandler'); + + try { + logger.info(`Updating cloud connector with id: ${request.params.id}`); + const cloudConnector = await cloudConnectorService.update( + internalSoClient, + request.params.id, + request.body + ); + logger.info('Successfully updated cloud connector'); + return response.ok({ body: cloudConnector }); + } catch (error) { + logger.error('Failed to update cloud connector', error.message); + return response.customError({ + statusCode: 400, + body: { + message: error.message, + }, + }); + } +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/cloud_connector.ts b/x-pack/platform/plugins/shared/fleet/server/services/cloud_connector.ts index 0dbea4c28b930..4bb08b4c40db8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/cloud_connector.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/cloud_connector.ts @@ -11,16 +11,20 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser import type { CloudConnectorSO, CloudConnectorListOptions, - CloudConnectorSecretVarValue, + CloudConnectorSecretReference, } from '../../common/types/models/cloud_connector'; import type { CloudConnectorSOAttributes } from '../types/so_attributes'; -import type { CreateCloudConnectorRequest } from '../routes/cloud_connector/handlers'; +import type { + CreateCloudConnectorRequest, + UpdateCloudConnectorRequest, +} from '../routes/cloud_connector/handlers'; import { CLOUD_CONNECTOR_SAVED_OBJECT_TYPE } from '../../common/constants'; import { CloudConnectorCreateError, CloudConnectorGetListError, CloudConnectorInvalidVarsError, + CloudConnectorUpdateError, } from '../errors'; import { appContextService } from './app_context'; @@ -34,6 +38,11 @@ export interface CloudConnectorServiceInterface { soClient: SavedObjectsClientContract, options?: CloudConnectorListOptions ): Promise; + update( + soClient: SavedObjectsClientContract, + id: string, + cloudConnector: UpdateCloudConnectorRequest + ): Promise; } export class CloudConnectorService implements CloudConnectorServiceInterface { @@ -138,6 +147,59 @@ export class CloudConnectorService implements CloudConnectorServiceInterface { } } + async update( + soClient: SavedObjectsClientContract, + id: string, + cloudConnector: UpdateCloudConnectorRequest + ): Promise { + const logger = this.getLogger('update'); + + try { + logger.info(`Updating cloud connector with id: ${id}`); + + // Get the existing cloud connector + const existingCloudConnector = await soClient.get( + CLOUD_CONNECTOR_SAVED_OBJECT_TYPE, + id + ); + + // Validate the update request if vars are provided + if (cloudConnector.vars) { + this.validateCloudConnectorDetails({ + name: cloudConnector.name || existingCloudConnector.attributes.name, + vars: cloudConnector.vars, + cloudProvider: + cloudConnector.cloudProvider || existingCloudConnector.attributes.cloudProvider, + }); + } + + // Prepare the update attributes + const updateAttributes: Partial = { + updated_at: new Date().toISOString(), + }; + + // Update the cloud connector + const updatedSavedObject = await soClient.update( + CLOUD_CONNECTOR_SAVED_OBJECT_TYPE, + id, + { ...updateAttributes, ...cloudConnector } + ); + + logger.info(`Successfully updated cloud connector with id: ${id}`); + + return { + id: updatedSavedObject.id, + ...existingCloudConnector.attributes, + ...updatedSavedObject.attributes, + }; + } catch (error) { + logger.error(`Failed to update cloud connector with id: ${id}`, error.message); + throw new CloudConnectorUpdateError( + `CloudConnectorService Failed to update cloud connector: ${error.message}\n${error.stack}` + ); + } + } + private validateCloudConnectorDetails(cloudConnector: CreateCloudConnectorRequest) { const logger = this.getLogger('validate cloud connector details'); const vars = cloudConnector.vars; @@ -152,7 +214,7 @@ export class CloudConnectorService implements CloudConnectorServiceInterface { // Check for AWS variables if (roleArn) { - const externalId: CloudConnectorSecretVarValue | undefined = vars.external_id?.value; + const externalId: CloudConnectorSecretReference | undefined = vars.external_id?.value; if (!externalId) { logger.error('Package policy must contain valid external_id secret reference'); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts b/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts index d76b65e1331c0..87f161b3ed0c2 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts @@ -98,6 +98,7 @@ import { PackageRollbackError, CloudConnectorInvalidVarsError, CloudConnectorCreateError, + CloudConnectorUpdateError, } from '../errors'; import { NewPackagePolicySchema, PackagePolicySchema, UpdatePackagePolicySchema } from '../types'; import type { @@ -109,10 +110,12 @@ import type { PostPackagePolicyCreateCallback, PostPackagePolicyPostCreateCallback, PutPackagePolicyPostUpdateCallback, + CloudConnectorSOAttributes, } from '../types'; import type { ExternalCallback } from '..'; import { + CLOUD_CONNECTOR_SAVED_OBJECT_TYPE, MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS_10, MAX_CONCURRENT_PACKAGE_ASSETS, @@ -481,11 +484,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient { secretReferences = secretsRes.secretReferences; inputs = enrichedPackagePolicy.inputs as PackagePolicyInput[]; + const cloudConnector = await this.createCloudConnectorForPackagePolicy( soClient, enrichedPackagePolicy, agentPolicies[0] ); + if (cloudConnector) { enrichedPackagePolicy.cloud_connector_id = cloudConnector.id; } @@ -2203,6 +2208,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { policy_id: newPolicy.policy_id ?? undefined, policy_ids: newPolicy.policy_ids ?? undefined, output_id: newPolicy.output_id, + cloud_connector_id: newPolicy.cloud_connector_id, inputs: newPolicy.inputs[0]?.streams ? newPolicy.inputs : inputs, vars: newPolicy.vars || newPP.vars, supports_agentless: newPolicy.supports_agentless, @@ -2866,37 +2872,56 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ): Promise { const logger = this.getLogger('createCloudConnectorForPackagePolicy'); - // Check if cloud connector setup supported and not already created - const isNewCloudConnectorSetup = - !!enrichedPackagePolicy?.supports_cloud_connector && - agentPolicy.agentless?.cloud_connectors?.enabled && - !enrichedPackagePolicy?.cloud_connector_id; - - if (!isNewCloudConnectorSetup) { - logger.debug( - `New cloud connector setup is not supported, supports_cloud_connector: ${enrichedPackagePolicy?.supports_cloud_connector}, agentless.cloud_connectors.enabled: ${agentPolicy.agentless?.cloud_connectors?.enabled}, cloud_connector_id: ${enrichedPackagePolicy?.cloud_connector_id}` - ); + if (!enrichedPackagePolicy?.supports_cloud_connector) { return; } + const cloudProvider = agentPolicy.agentless?.cloud_connectors?.target_csp as CloudProvider; - try { - const cloudConnectorVars = extractPackagePolicyVars( - cloudProvider, - enrichedPackagePolicy, - logger - ); - if (cloudConnectorVars) { - const cloudConnector = await cloudConnectorService.create(soClient, { - name: `${cloudProvider}-cloud-connector: ${enrichedPackagePolicy.name}`, - vars: cloudConnectorVars, - cloudProvider, - }); - logger.info(`Successfully created cloud connector: ${cloudConnector.id}`); - return cloudConnector; + const cloudConnectorVars = extractPackagePolicyVars( + cloudProvider, + enrichedPackagePolicy, + logger + ); + + if (cloudConnectorVars) { + if ( + enrichedPackagePolicy?.supports_cloud_connector && + enrichedPackagePolicy?.cloud_connector_id + ) { + const existingCloudConnector = await soClient.get( + CLOUD_CONNECTOR_SAVED_OBJECT_TYPE, + enrichedPackagePolicy.cloud_connector_id + ); + + try { + const cloudConnector = await cloudConnectorService.update( + soClient, + enrichedPackagePolicy.cloud_connector_id, + { + vars: cloudConnectorVars, + packagePolicyCount: existingCloudConnector.attributes.packagePolicyCount + 1, + } + ); + logger.info(`Successfully updated cloud connector: ${cloudConnector.id}`); + return cloudConnector; + } catch (e) { + logger.error(`Error updating cloud connector: ${e}`); + throw new CloudConnectorUpdateError(`${e}`); + } + } else { + try { + const cloudConnector = await cloudConnectorService.create(soClient, { + name: `${cloudProvider}-cloud-connector: ${enrichedPackagePolicy.name}`, + vars: cloudConnectorVars, + cloudProvider, + }); + logger.info(`Successfully created cloud connector: ${cloudConnector.id}`); + return cloudConnector; + } catch (error) { + logger.error(`Error creating cloud connector: ${error}`); + throw new CloudConnectorCreateError(`${error}`); + } } - } catch (error) { - logger.error(`Error creating cloud connector: ${error}`); - throw new CloudConnectorCreateError(`${error}`); } } } diff --git a/x-pack/platform/plugins/shared/fleet/server/services/secrets.ts b/x-pack/platform/plugins/shared/fleet/server/services/secrets.ts index 6d49219fc64b1..16d4d8946c560 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/secrets.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/secrets.ts @@ -265,6 +265,19 @@ export async function extractAndWriteSecrets(opts: { if (!secretPaths.length) { return { packagePolicy, secretReferences: [] }; } + const cloudConnectorsSecretReferences = + packagePolicy.supports_cloud_connector && packagePolicy.cloud_connector_id + ? getCloudConnectorSecretReferences(packagePolicy, secretPaths) + : []; + + const hasCloudConnectorSecretReferences = + packagePolicy.supports_cloud_connector && + packagePolicy.cloud_connector_id && + cloudConnectorsSecretReferences.length; + + if (hasCloudConnectorSecretReferences) { + return { packagePolicy, secretReferences: cloudConnectorsSecretReferences }; + } const secretsToCreate = secretPaths.filter( (secretPath) => !!secretPath.value.value && !secretPath.value.value.isSecretRef @@ -280,12 +293,6 @@ export async function extractAndWriteSecrets(opts: { secrets, packagePolicy ); - - const cloudConnectorsSecretReferences = getCloudConnectorSecretReferences( - packagePolicy, - secretPaths - ); - return { packagePolicy: policyWithSecretRefs, secretReferences: [ @@ -295,7 +302,6 @@ export async function extractAndWriteSecrets(opts: { } return [...acc, { id: secret.id }]; }, []), - ...cloudConnectorsSecretReferences, ], }; } @@ -506,6 +512,13 @@ export async function isOutputSecretStorageEnabled( return true; } + // Enable output secrets storage in development environment + const isDevelopment = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev'; + if (isDevelopment) { + logger.debug('Output secrets storage is enabled for development environment'); + return true; + } + // now check the flag in settings to see if the fleet server requirement has already been met // once the requirement has been met, output secrets are always on const settings = await settingsService.getSettingsOrUndefined(soClient); @@ -699,13 +712,20 @@ function getCloudConnectorSecretReferences( packagePolicy: NewPackagePolicy, secretPaths: SecretPath[] ): PolicySecretReference[] { - return packagePolicy?.supports_cloud_connector - ? secretPaths - .filter((secretPath) => !!secretPath.value.value && secretPath.value.value?.isSecretRef) - .map((secretPath) => ({ - id: secretPath.value.value?.id, - })) - : []; + // For cloud connectors, we need to find secret paths that are already secret references + if (!packagePolicy?.supports_cloud_connector || !packagePolicy.cloud_connector_id) { + return []; + } + return secretPaths + .filter( + (secretPath) => + !!secretPath.value?.value && + typeof secretPath.value.value === 'object' && + secretPath.value.value?.id + ) + .map((secretPath) => ({ + id: secretPath.value.value?.id, + })); } /** diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx index d62cc89f9a653..8731db0552f6c 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx @@ -48,6 +48,7 @@ import { ReadDocumentation } from '../common'; import { CloudFormationCloudCredentialsGuide } from './aws_cloud_formation_credential_guide'; import type { UpdatePolicy } from '../types'; import { useCloudSetup } from '../hooks/use_cloud_setup_context'; +import { CloudConnectorSetup } from '../cloud_connector/cloud_connector_setup'; interface AwsAgentlessFormProps { input: NewPackagePolicyInput; @@ -82,6 +83,7 @@ export const AwsCredentialsFormAgentless = ({ if (!getAwsCredentialsType(input)) { updatePolicy({ updatedPolicy: { + supports_cloud_connector: awsCredentialsType === AWS_CREDENTIALS_TYPE.CLOUD_CONNECTORS, ...updatePolicyWithInputs(newPolicy, awsPolicyType, { 'aws.credentials.type': { value: awsCredentialsType, @@ -161,7 +163,6 @@ export const AwsCredentialsFormAgentless = ({ const templateUrl = showCloudFormationAccordion ? cloudFormationSettings[awsCredentialsType].templateUrl : ''; - return ( <> @@ -211,9 +212,13 @@ export const AwsCredentialsFormAgentless = ({ options={selectorOptions()} disabled={!!disabled} onChange={(optionId) => { + const updatedNewPolicy = { + ...newPolicy, + supports_cloud_connector: optionId === AWS_CREDENTIALS_TYPE.CLOUD_CONNECTORS, + }; updatePolicy({ updatedPolicy: updatePolicyWithInputs( - newPolicy, + updatedNewPolicy, awsPolicyType, getCloudCredentialVarsConfig({ setupTechnology, @@ -237,49 +242,68 @@ export const AwsCredentialsFormAgentless = ({ )} - {showCloudFormationAccordion && ( + + {awsCredentialsType !== AWS_CREDENTIALS_TYPE.CLOUD_CONNECTORS && ( <> - - - - - - - - - + {showCloudFormationAccordion && ( + <> + + + + + + + + + + + )} + { + const updatedPolicy = updatePolicyWithInputs(newPolicy, awsPolicyType, { + [key]: { value }, + }); + updatePolicy({ + updatedPolicy, + }); + }} + hasInvalidRequiredVars={hasInvalidRequiredVars} + /> )} - { - const updatedPolicy = updatePolicyWithInputs(newPolicy, awsPolicyType, { - [key]: { value }, - }); - updatePolicy({ - updatedPolicy, - }); - }} - hasInvalidRequiredVars={hasInvalidRequiredVars} - /> + + {showCloudConnectors && awsCredentialsType === AWS_CREDENTIALS_TYPE.CLOUD_CONNECTORS && ( + + )} ); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_connector_form.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_connector_form.tsx new file mode 100644 index 0000000000000..6b7ea57772ed5 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_connector_form.tsx @@ -0,0 +1,109 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; + +import { EuiAccordion, EuiSpacer, EuiButton, EuiLink } from '@elastic/eui'; +import { type PackagePolicyVars, type AWSCloudConnectorFormProps } from '../types'; +import { CloudFormationCloudCredentialsGuide } from './aws_cloud_formation_guide'; +import { + updatePolicyWithAwsCloudConnectorCredentials, + getCloudConnectorRemoteRoleTemplate, + updateInputVarsWithCredentials, +} from '../utils'; +import { CLOUD_CONNECTOR_FIELD_NAMES } from '../constants'; +import { getAwsCloudConnectorsCredentialsFormOptions } from './aws_cloud_connector_options'; +import { CloudConnectorInputFields } from '../form/cloud_connector_input_fields'; + +export const AWSCloudConnectorForm: React.FC = ({ + input, + newPolicy, + packageInfo, + cloud, + updatePolicy, + hasInvalidRequiredVars = false, + isOrganization = false, + templateName, + credentials, + setCredentials, +}) => { + const cloudConnectorRemoteRoleTemplate = + cloud && templateName + ? getCloudConnectorRemoteRoleTemplate({ + input, + cloud, + packageInfo, + templateName, + }) + : undefined; + const inputVars = input.streams.find((i) => i.enabled)?.vars; + + // Update inputVars with current credentials using utility function or inputVars if no credentials are provided + const updatedInputVars = credentials + ? updateInputVarsWithCredentials(inputVars as PackagePolicyVars, credentials, true) + : inputVars; + + const fields = getAwsCloudConnectorsCredentialsFormOptions(updatedInputVars); + + return ( + <> + {'Steps to assume role'}} + paddingSize="l" + > + + + + + + + + + {fields && ( + { + // Update local credentials state if available + if (credentials) { + const updatedCredentials = { ...credentials }; + if ( + key === CLOUD_CONNECTOR_FIELD_NAMES.ROLE_ARN || + key === CLOUD_CONNECTOR_FIELD_NAMES.AWS_ROLE_ARN + ) { + updatedCredentials.roleArn = value; + } else if ( + key === CLOUD_CONNECTOR_FIELD_NAMES.EXTERNAL_ID || + key === CLOUD_CONNECTOR_FIELD_NAMES.AWS_EXTERNAL_ID + ) { + updatedCredentials.externalId = value; + } + setCredentials(updatedCredentials); + } else { + // Fallback to old method + updatePolicy({ + updatedPolicy: updatePolicyWithAwsCloudConnectorCredentials(newPolicy, input, { + [key]: value, + }), + }); + } + }} + hasInvalidRequiredVars={hasInvalidRequiredVars} + /> + )} + + ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_connector_options.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_connector_options.tsx new file mode 100644 index 0000000000000..1257f53fb5395 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_connector_options.tsx @@ -0,0 +1,110 @@ +/* + * 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 { PackagePolicyConfigRecord } from '@kbn/fleet-plugin/common'; +import { i18n } from '@kbn/i18n'; + +// Cloud Connector field labels +const AWS_CLOUD_CONNECTOR_FIELD_LABELS = { + role_arn: i18n.translate('securitySolutionPackages.awsIntegration.roleArnLabel', { + defaultMessage: 'Role ARN', + }), + external_id: i18n.translate('securitySolutionPackages.awsIntegration.externalId', { + defaultMessage: 'External ID', + }), +} as const; + +// Cloud Connector options interface +export interface AwsCloudConnectorOptions { + id: string; + label: string; + type?: 'text' | 'password'; + dataTestSubj: string; + isSecret?: boolean; + value: string; +} + +// Define field sequence order +const FIELD_SEQUENCE = [ + 'role_arn', + 'aws.role_arn', + 'aws.credentials.external_id', + 'external_id', +] as const; + +export const getAwsCloudConnectorsCredentialsFormOptions = ( + inputVars?: PackagePolicyConfigRecord | undefined +) => { + if (!inputVars) { + return; + } + + const fields: ({ + label: string; + type?: 'text' | 'password' | undefined; + isSecret?: boolean | undefined; + dataTestSubj: string; + } & { + value: string; + id: string; + dataTestSubj: string; + })[] = []; + + // Create a map of all available fields + const availableFields = new Map(); + + if (inputVars.role_arn) { + availableFields.set('role_arn', { + id: 'role_arn', + label: AWS_CLOUD_CONNECTOR_FIELD_LABELS.role_arn, + type: 'text' as const, + dataTestSubj: 'awsCloudConnectorRoleArnInput', + value: inputVars.role_arn.value, + }); + } + + if (inputVars['aws.role_arn']) { + availableFields.set('aws.role_arn', { + id: 'aws.role_arn', + label: AWS_CLOUD_CONNECTOR_FIELD_LABELS.role_arn, + type: 'text' as const, + dataTestSubj: 'awsCloudConnectorRoleArnInput', + value: inputVars['aws.role_arn'].value, + }); + } + + if (inputVars['aws.credentials.external_id']) { + availableFields.set('aws.credentials.external_id', { + id: 'aws.credentials.external_id', + label: AWS_CLOUD_CONNECTOR_FIELD_LABELS.external_id, + type: 'password' as const, + dataTestSubj: 'awsCloudConnectorExternalId', + isSecret: true, + value: inputVars['aws.credentials.external_id'].value, + }); + } + + if (inputVars.external_id) { + availableFields.set('external_id', { + id: 'external_id', + label: AWS_CLOUD_CONNECTOR_FIELD_LABELS.external_id, + type: 'password' as const, + dataTestSubj: 'awsCloudConnectorExternalId', + isSecret: true, + value: inputVars.external_id.value, + }); + } + + // Build fields array in sequence order + FIELD_SEQUENCE.forEach((fieldId) => { + const field = availableFields.get(fieldId); + if (field) { + fields.push(field); + } + }); + + return fields; +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_formation_guide.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_formation_guide.tsx new file mode 100644 index 0000000000000..f4d2f9a589976 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_cloud_formation_guide.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export interface CloudFormationCloudCredentialsGuideProps { + isOrganization?: boolean; +} + +export const CloudFormationCloudCredentialsGuide: React.FC< + CloudFormationCloudCredentialsGuideProps +> = ({ isOrganization = false }) => { + return ( +
+ +
    + {isOrganization ? ( +
  1. + {'admin'}, + }} + /> +
  2. + ) : ( +
  3. + {'admin'}, + }} + /> +
  4. + )} +
  5. + {'Launch CloudFormation'}, + }} + /> +
  6. +
  7. + {'AWS region'}, + }} + /> +
  8. +
  9. + + + + ), + capabilities: ( + + + + ), + }} + /> +
  10. +
  11. + {'Create stack'}, + }} + /> +
  12. +
  13. + {'CREATE_COMPLETE'}, + }} + /> +
  14. +
  15. + {'Role ARN'}, + external_id: {'External ID'}, + }} + /> +
  16. +
+
+
+ ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_reusable_connector_form.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_reusable_connector_form.tsx new file mode 100644 index 0000000000000..beaa60381b2be --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/aws_cloud_connector/aws_reusable_connector_form.tsx @@ -0,0 +1,95 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiSpacer, EuiComboBox, EuiFormRow, EuiText } from '@elastic/eui'; +import type { CloudConnectorOption, ComboBoxOption } from '../types'; +import { useGetCloudConnectors } from '../hooks/use_get_cloud_connectors'; +import type { CloudConnectorCredentials } from '../hooks/use_cloud_connector_setup'; + +export const AWSReusableConnectorForm: React.FC<{ + cloudConnectorId: string | undefined; + isEditPage: boolean; + credentials: CloudConnectorCredentials; + setCredentials: (credentials: CloudConnectorCredentials) => void; +}> = ({ credentials, setCredentials, isEditPage, cloudConnectorId }) => { + const { data: cloudConnectors = [] } = useGetCloudConnectors(); + + // Convert cloud connectors to combo box options (only standard properties for EuiComboBox) + const comboBoxOptions: ComboBoxOption[] = cloudConnectors.map((connector) => ({ + label: connector.name, + value: connector.id, // Use ID as value for easier lookup + })); + + // Keep full connector data for reference + const cloudConnectorData: CloudConnectorOption[] = cloudConnectors.map((connector) => ({ + label: connector.name, + value: connector.id, + id: connector.id, + roleArn: connector.vars.role_arn || connector.vars['aws.role_arn'], + externalId: connector.vars['aws.credentials.external_id'] || connector.vars.external_id, + })); + + // Find the currently selected connector based on credentials + const selectedConnector = useMemo(() => { + return (isEditPage && cloudConnectorId) || credentials?.cloudConnectorId + ? comboBoxOptions.find( + (opt) => opt.value === credentials.cloudConnectorId || opt.value === cloudConnectorId + ) || null + : null; + }, [isEditPage, cloudConnectorId, credentials?.cloudConnectorId, comboBoxOptions]); + + const handleConnectorChange = useCallback( + (selected: Array<{ label: string; value?: string }>) => { + const [selectedOption] = selected; + + if (selectedOption?.value) { + const connector = cloudConnectorData.find((opt) => opt.id === selectedOption.value); + if (connector?.roleArn && connector?.externalId) { + setCredentials({ + roleArn: connector.roleArn.value, + externalId: connector.externalId.value, + cloudConnectorId: connector.id, + }); + } + } else { + setCredentials({ + roleArn: undefined, + externalId: undefined, + cloudConnectorId: undefined, + }); + } + }, + [cloudConnectorData, setCredentials] + ); + + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_setup.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_setup.tsx new file mode 100644 index 0000000000000..f4a01b97c6cf5 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_setup.tsx @@ -0,0 +1,195 @@ +/* + * 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, { useState, useEffect, useCallback } from 'react'; +import { EuiSpacer, EuiText, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { + NewPackagePolicy, + NewPackagePolicyInput, + PackageInfo, +} from '@kbn/fleet-plugin/common'; +import type { CloudSetup } from '@kbn/cloud-plugin/public'; + +import { NewCloudConnectorForm } from './form/new_cloud_connector_form'; +import { ReusableCloudConnectorForm } from './form/reusable_cloud_connector_form'; +import { useGetCloudConnectors } from './hooks/use_get_cloud_connectors'; +import { useCloudConnectorSetup } from './hooks/use_cloud_connector_setup'; +import { CloudConnectorTabs, type CloudConnectorTab } from './cloud_connector_tabs'; +import type { UpdatePolicy } from '../types'; +import { TABS, CLOUD_FORMATION_EXTERNAL_DOC_URL } from './constants'; + +export interface CloudConnectorSetupProps { + input: NewPackagePolicyInput; + newPolicy: NewPackagePolicy; + packageInfo: PackageInfo; + updatePolicy: UpdatePolicy; + isEditPage?: boolean; + hasInvalidRequiredVars: boolean; + cloud?: CloudSetup; + cloudProvider?: string; + templateName: string; +} + +export const CloudConnectorSetup: React.FC = ({ + input, + newPolicy, + packageInfo, + updatePolicy, + isEditPage = false, + hasInvalidRequiredVars, + cloud, + cloudProvider, + templateName, +}) => { + const isCloudConnectorReusableEnabled = true; + const [selectedTabId, setSelectedTabId] = useState('new-connection'); + + const { data: cloudConnectors } = useGetCloudConnectors(); + const cloudConnectorsCount = cloudConnectors?.length; + + // Use the cloud connector setup hook + const { + newConnectionCredentials, + setNewConnectionCredentials, + existingConnectionCredentials, + setExistingConnectionCredentials, + updatePolicyWithNewCredentials, + updatePolicyWithExistingCredentials, + } = useCloudConnectorSetup(input, newPolicy, updatePolicy); + + // Auto-select existing-connection tab when cloud connectors are available + useEffect(() => { + if (cloudConnectorsCount && cloudConnectorsCount > 0) { + setSelectedTabId(TABS.EXISTING_CONNECTION); + } + }, [cloudConnectorsCount]); + + const tabs: CloudConnectorTab[] = [ + { + id: TABS.NEW_CONNECTION, + name: ( + + ), + content: ( + <> + +
+ + + + + ), + }} + /> + +
+ + + + ), + }, + { + id: TABS.EXISTING_CONNECTION, + name: ( + + ), + content: ( + + ), + }, + ]; + + const onTabClick = useCallback( + (tab: { id: string }) => { + setSelectedTabId(tab.id); + + if (tab.id === TABS.NEW_CONNECTION && newConnectionCredentials.roleArn) { + updatePolicyWithNewCredentials(newConnectionCredentials); + } else if ( + tab.id === TABS.EXISTING_CONNECTION && + existingConnectionCredentials.cloudConnectorId + ) { + updatePolicyWithExistingCredentials(existingConnectionCredentials); + } + }, + [ + newConnectionCredentials, + existingConnectionCredentials, + updatePolicyWithNewCredentials, + updatePolicyWithExistingCredentials, + ] + ); + + return ( + <> + {!isCloudConnectorReusableEnabled && ( + + )} + {isCloudConnectorReusableEnabled && ( + + )} + + ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_tabs.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_tabs.tsx new file mode 100644 index 0000000000000..6f72e93faf2e9 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_tabs.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiTab, EuiTabs, EuiSpacer } from '@elastic/eui'; + +export interface CloudConnectorTab { + id: string; + name: string; + content: React.ReactNode; +} + +export interface CloudConnectorTabsProps { + tabs: CloudConnectorTab[]; + selectedTabId: string; + onTabClick: (tab: CloudConnectorTab) => void; + isEditPage: boolean; + cloudProvider: string | undefined; + cloudConnectorsCount: number; +} + +export const CloudConnectorTabs: React.FC = ({ + tabs, + selectedTabId, + onTabClick, + cloudConnectorsCount, + isEditPage, +}) => { + return ( + <> + + + {tabs.map((tab) => ( + { + onTabClick(tab); + }} + isSelected={tab.id === selectedTabId} + disabled={isEditPage && !cloudConnectorsCount} + > + {tab.name} + + ))} + + + {tabs.find((tab) => tab.id === selectedTabId)?.content} + + ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/constants.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/constants.ts new file mode 100644 index 0000000000000..9bfb55a5055c5 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/constants.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. + */ +export const TEMPLATE_URL_ACCOUNT_TYPE_ENV_VAR = 'ACCOUNT_TYPE'; +export const TEMPLATE_URL_ELASTIC_RESOURCE_ID_ENV_VAR = 'RESOURCE_ID'; +export const CLOUD_FORMATION_TEMPLATE_URL_CLOUD_CONNECTORS = + 'cloud_formation_cloud_connectors_template'; + +export const AWS_SINGLE_ACCOUNT = 'single-account'; +export const AWS_ORGANIZATION_ACCOUNT = 'organization-account'; + +export const AWS_CREDENTIALS_TYPE = { + CLOUD_CONNECTORS: 'cloud_connectors', +}; + +export const TABS = { + NEW_CONNECTION: 'new-connection', + EXISTING_CONNECTION: 'existing-connection', +} as const; + +export const CLOUD_FORMATION_EXTERNAL_DOC_URL = + 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html'; + +export const CLOUD_CONNECTOR_FIELD_NAMES = { + ROLE_ARN: 'role_arn', + EXTERNAL_ID: 'external_id', + AWS_ROLE_ARN: 'aws.role_arn', + AWS_EXTERNAL_ID: 'aws.credentials.external_id', +} as const; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_input_fields.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_input_fields.tsx new file mode 100644 index 0000000000000..f8e5fe95367c8 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_input_fields.tsx @@ -0,0 +1,106 @@ +/* + * 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, { Suspense } from 'react'; +import { EuiFieldText, EuiFormRow, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; +import type { PackageInfo } from '@kbn/fleet-plugin/common'; +import { css } from '@emotion/react'; +import { LazyPackagePolicyInputVarField } from '@kbn/fleet-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { findVariableDef, fieldIsInvalid } from '../../utils'; +import type { CloudConnectorField } from '../types'; + +export const CloudConnectorInputFields = ({ + fields, + onChange, + packageInfo, + hasInvalidRequiredVars = false, +}: { + fields: Array; + onChange: (key: string, value: string) => void; + packageInfo: PackageInfo; + hasInvalidRequiredVars?: boolean; +}) => { + // Helper styles for password fields + const passwordFieldStyles = css` + width: 100%; + .euiFormControlLayout, + .euiFormControlLayout__childrenWrapper, + .euiFormRow, + input { + max-width: 100%; + width: 100%; + } + `; + + // Helper to get error message + const getInvalidError = (label: string) => + i18n.translate('securitySolutionPackages.cspmIntegration.integration.fieldRequired', { + defaultMessage: '{field} is required', + values: { field: label }, + }); + + return ( +
+ {fields.map((field) => { + const invalid = fieldIsInvalid(field.value, hasInvalidRequiredVars); + const invalidError = getInvalidError(field.label); + + if (field.type === 'password' && field.isSecret === true) { + return ( + + +
+ }> + onChange(field.id, value)} + errors={invalid ? [invalidError] : []} + forceShowErrors={invalid} + isEditPage={true} + /> + +
+ +
+ ); + } + + if (field.type === 'text') { + return ( + + onChange(field.id, event.target.value)} + data-test-subj={field.dataTestSubj} + /> + + ); + } + + return null; + })} +
+ ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/new_cloud_connector_form.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/new_cloud_connector_form.tsx new file mode 100644 index 0000000000000..a3a82a9210229 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/new_cloud_connector_form.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 type { NewCloudConnectorFormProps } from '../types'; +import { AWSCloudConnectorForm } from '../aws_cloud_connector/aws_cloud_connector_form'; + +export const NewCloudConnectorForm: React.FC = ({ + input, + newPolicy, + packageInfo, + updatePolicy, + isEditPage = false, + cloud, + cloudProvider, + templateName, + credentials, + setCredentials, + hasInvalidRequiredVars, +}) => { + // Default to AWS if no cloudProvider is specified + const provider = cloudProvider || 'aws'; + + switch (provider) { + case 'aws': + return ( + + ); + case 'gcp': + case 'azure': + // TODO: Implement GCP and Azure cloud connector forms + return null; + default: + return null; + } +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/reusable_cloud_connector_form.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/reusable_cloud_connector_form.tsx new file mode 100644 index 0000000000000..cc3f23e0b56ae --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/reusable_cloud_connector_form.tsx @@ -0,0 +1,39 @@ +/* + * 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 type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; +import { AWSReusableConnectorForm } from '../aws_cloud_connector/aws_reusable_connector_form'; +import type { CloudConnectorCredentials } from '../hooks/use_cloud_connector_setup'; + +export const ReusableCloudConnectorForm: React.FC<{ + credentials: CloudConnectorCredentials; + setCredentials: (credentials: CloudConnectorCredentials) => void; + newPolicy: NewPackagePolicy; + cloudProvider: string; + isEditPage: boolean; +}> = ({ credentials, setCredentials, cloudProvider, newPolicy, isEditPage }) => { + const provider = cloudProvider || 'aws'; + + switch (provider) { + case 'aws': + return ( + + ); + case 'gcp': + case 'azure': + // TODO: Implement GCP and Azure cloud connector forms + return null; + default: + return null; + } +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/index.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/index.ts new file mode 100644 index 0000000000000..902e4287a7d32 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { useGetCloudConnectors } from './use_get_cloud_connectors'; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.ts new file mode 100644 index 0000000000000..f655924952602 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.ts @@ -0,0 +1,116 @@ +/* + * 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 { useState, useCallback } from 'react'; +import type { NewPackagePolicy, NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; +import type { UpdatePolicy } from '../../types'; +import { updatePolicyInputs, updateInputVarsWithCredentials } from '../utils'; +import type { PackagePolicyVars } from '../types'; +export interface CloudConnectorCredentials { + roleArn?: string; + externalId?: string; + cloudConnectorId?: string; +} + +export interface UseCloudConnectorSetupReturn { + // State for new connection form + newConnectionCredentials: CloudConnectorCredentials; + setNewConnectionCredentials: (credentials: CloudConnectorCredentials) => void; + + // State for existing connection form + existingConnectionCredentials: CloudConnectorCredentials; + setExistingConnectionCredentials: (credentials: CloudConnectorCredentials) => void; + + // Update policy callbacks + updatePolicyWithNewCredentials: (credentials: CloudConnectorCredentials) => void; + updatePolicyWithExistingCredentials: (credentials: CloudConnectorCredentials) => void; +} + +export const useCloudConnectorSetup = ( + input: NewPackagePolicyInput, + newPolicy: NewPackagePolicy, + updatePolicy: UpdatePolicy +): UseCloudConnectorSetupReturn => { + // State for new connection form + const [newConnectionCredentials, setNewConnectionCredentials] = + useState({ roleArn: undefined, externalId: undefined }); + + // State for existing connection form + const [existingConnectionCredentials, setExistingConnectionCredentials] = + useState({ + roleArn: undefined, + externalId: undefined, + cloudConnectorId: undefined, + }); + + // Update policy with new connection credentials + const updatePolicyWithNewCredentials = useCallback( + (credentials: CloudConnectorCredentials) => { + const updatedPolicy = { ...newPolicy }; + const inputVars = input.streams.find((i) => i.enabled)?.vars; + + // Handle undefined cases safely + const updatedInputVars = updateInputVarsWithCredentials( + inputVars as PackagePolicyVars, + credentials, + true + ); + setNewConnectionCredentials(credentials); + + // Apply updatedVars to the policy using utility function + if (inputVars) { + const updatedPolicyWithInputs = updatePolicyInputs( + updatedPolicy, + updatedInputVars as PackagePolicyVars + ); + updatedPolicy.inputs = updatedPolicyWithInputs.inputs; + } + updatedPolicy.cloud_connector_id = undefined; + + updatePolicy({ updatedPolicy }); + }, + [input.streams, newPolicy, updatePolicy] + ); + + // Update policy with existing connection credentials + const updatePolicyWithExistingCredentials = useCallback( + (credentials: CloudConnectorCredentials) => { + const updatedPolicy = { ...newPolicy }; + const inputVars = input.streams.find((i) => i.enabled)?.vars; + + // Handle undefined cases safely + const updatedInputVars = updateInputVarsWithCredentials( + inputVars as PackagePolicyVars, + credentials + ); + // Update existing connection credentials state + setExistingConnectionCredentials(credentials); + + // Apply updatedVars to the policy + if (inputVars) { + const updatedPolicyWithInputs = updatePolicyInputs( + updatedPolicy, + updatedInputVars as PackagePolicyVars + ); + updatedPolicy.inputs = updatedPolicyWithInputs.inputs; + } + updatedPolicy.cloud_connector_id = credentials.cloudConnectorId; + + updatePolicy({ updatedPolicy }); + }, + [input.streams, newPolicy, updatePolicy] + ); + + return { + newConnectionCredentials, + setNewConnectionCredentials, + existingConnectionCredentials, + setExistingConnectionCredentials, + updatePolicyWithNewCredentials, + updatePolicyWithExistingCredentials, + }; +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_get_cloud_connectors.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_get_cloud_connectors.ts new file mode 100644 index 0000000000000..fcb1954543aa0 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_get_cloud_connectors.ts @@ -0,0 +1,39 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import type { CloudConnectorSO, CloudConnectorListOptions } from '@kbn/fleet-plugin/public'; +import { CLOUD_CONNECTOR_API_ROUTES } from '@kbn/fleet-plugin/public'; +import type { CoreStart, HttpStart } from '@kbn/core/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +const fetchCloudConnectors = async ( + http: HttpStart, + options?: CloudConnectorListOptions +): Promise => { + const queryParams = new URLSearchParams(); + + if (options?.page !== undefined) { + queryParams.append('page', options.page.toString()); + } + + if (options?.perPage !== undefined) { + queryParams.append('perPage', options.perPage.toString()); + } + + const url = `${CLOUD_CONNECTOR_API_ROUTES.LIST_PATTERN}`; + + return http.get(url); +}; + +export const useGetCloudConnectors = () => { + const CLOUD_CONNECTOR_QUERY_KEY = 'get-cloud-connectors'; + const { http } = useKibana().services; + return useQuery([CLOUD_CONNECTOR_QUERY_KEY], () => fetchCloudConnectors(http), { + enabled: true, + }); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/types.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/types.ts new file mode 100644 index 0000000000000..bd60f4be64159 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/types.ts @@ -0,0 +1,119 @@ +/* + * 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 { ReactNode } from 'react'; +import type { + NewPackagePolicy, + NewPackagePolicyInput, + PackageInfo, +} from '@kbn/fleet-plugin/common'; +import type { CloudSetup } from '@kbn/cloud-plugin/public'; +import type { CloudConnectorSecretVar } from '@kbn/fleet-plugin/public'; +import type { CloudConnectorRoleArn } from '@kbn/fleet-plugin/common/types'; +import type { UpdatePolicy } from '../types'; +import type { CloudConnectorCredentials } from './hooks/use_cloud_connector_setup'; + +export interface CloudConnectorConfig { + provider: 'aws' | 'gcp' | 'azure'; + fields: CloudConnectorField[]; + description?: ReactNode; +} + +export interface NewCloudConnectorFormProps { + input: NewPackagePolicyInput; + newPolicy: NewPackagePolicy; + packageInfo: PackageInfo; + updatePolicy: UpdatePolicy; + isEditPage?: boolean; + hasInvalidRequiredVars: boolean; + cloud?: CloudSetup; + cloudProvider?: string; + templateName?: string; + credentials?: CloudConnectorCredentials; + setCredentials: (credentials: CloudConnectorCredentials) => void; +} + +// Define the interface for connector options +export interface CloudConnectorOption { + label: string; + value: string; + id: string; + roleArn?: CloudConnectorRoleArn; + externalId?: CloudConnectorSecretVar; +} + +// Interface for EuiComboBox options (only standard properties) +export interface ComboBoxOption { + label: string; + value: string; +} + +export interface AWSCloudConnectorFormProps { + input: NewPackagePolicyInput; + newPolicy: NewPackagePolicy; + packageInfo: PackageInfo; + updatePolicy: UpdatePolicy; + isEditPage?: boolean; + hasInvalidRequiredVars: boolean; + cloud?: CloudSetup; + cloudProvider?: string; + isOrganization?: boolean; + templateName?: string; + credentials?: CloudConnectorCredentials; + setCredentials: (credentials: CloudConnectorCredentials) => void; +} + +export interface CloudFormationCloudCredentialsGuideProps { + cloudProvider?: string; +} + +export interface CloudConnectorField { + label: string; + type?: 'text' | 'password' | undefined; + isSecret?: boolean | undefined; + dataTestSubj: string; + value: string; + id: string; +} + +export interface GetCloudConnectorRemoteRoleTemplateParams { + input: NewPackagePolicyInput; + cloud: Pick< + CloudSetup, + | 'isCloudEnabled' + | 'cloudId' + | 'cloudHost' + | 'deploymentUrl' + | 'serverless' + | 'isServerlessEnabled' + >; + packageInfo: PackageInfo; + templateName: string; +} + +/** + * Updates input variables with current credentials + */ +export interface InputVar { + value?: string | undefined; + type?: string; + [key: string]: unknown; +} + +export interface InputVars { + [key: string]: InputVar; +} + +// Type alias for the actual vars type from Fleet +export type PackagePolicyVars = + | Record< + string, + | { value?: string; type?: string; [key: string]: unknown } + | CloudConnectorSecretVar + | undefined + > + | undefined; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.ts new file mode 100644 index 0000000000000..92b47be70be43 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.ts @@ -0,0 +1,262 @@ +/* + * 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 { + NewPackagePolicy, + NewPackagePolicyInput, + PackageInfo, +} from '@kbn/fleet-plugin/common'; +import type { GetCloudConnectorRemoteRoleTemplateParams, PackagePolicyVars } from './types'; + +import type { CloudConnectorCredentials } from './hooks/use_cloud_connector_setup'; +import { + AWS_SINGLE_ACCOUNT, + CLOUD_CONNECTOR_FIELD_NAMES, + CLOUD_FORMATION_TEMPLATE_URL_CLOUD_CONNECTORS, + TEMPLATE_URL_ACCOUNT_TYPE_ENV_VAR, + TEMPLATE_URL_ELASTIC_RESOURCE_ID_ENV_VAR, +} from './constants'; + +const getCloudProviderFromCloudHost = (cloudHost: string | undefined): string | undefined => { + if (!cloudHost) return undefined; + const match = cloudHost.match(/\b(aws|gcp|azure)\b/)?.[1]; + return match; +}; + +const getDeploymentIdFromUrl = (url: string | undefined): string | undefined => { + if (!url) return undefined; + const match = url.match(/\/deployments\/([^/?#]+)/); + return match?.[1]; +}; + +const getKibanaComponentId = (cloudId: string | undefined): string | undefined => { + if (!cloudId) return undefined; + + const base64Part = cloudId.split(':')[1]; + const decoded = atob(base64Part); + const [, , kibanaComponentId] = decoded.split('$'); + + return kibanaComponentId || undefined; +}; + +export const getTemplateUrlFromPackageInfo = ( + packageInfo: PackageInfo | undefined, + integrationType: string, + templateUrlFieldName: string +): string | undefined => { + if (!packageInfo?.policy_templates) return undefined; + + const policyTemplate = packageInfo.policy_templates.find((p) => p.name === integrationType); + if (!policyTemplate) return undefined; + + 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 === templateUrlFieldName)?.default; + return template ? String(template) : acc; + }, ''); + return cloudFormationTemplate !== '' ? cloudFormationTemplate : undefined; + } +}; + +export const getCloudConnectorRemoteRoleTemplate = ({ + input, + cloud, + packageInfo, + templateName, +}: GetCloudConnectorRemoteRoleTemplateParams): string | undefined => { + let elasticResourceId: string | undefined; + const accountType = input?.streams?.[0]?.vars?.['aws.account_type']?.value ?? AWS_SINGLE_ACCOUNT; + + const provider = getCloudProviderFromCloudHost(cloud?.cloudHost); + + if (!provider || provider !== 'aws') return undefined; + + const deploymentId = getDeploymentIdFromUrl(cloud?.deploymentUrl); + + const kibanaComponentId = getKibanaComponentId(cloud?.cloudId); + + if (cloud?.isServerlessEnabled && cloud?.serverless?.projectId) { + elasticResourceId = cloud.serverless.projectId; + } + + if (cloud?.isCloudEnabled && deploymentId && kibanaComponentId) { + elasticResourceId = kibanaComponentId; + } + + if (!elasticResourceId) return undefined; + + return getTemplateUrlFromPackageInfo( + packageInfo, + templateName, + CLOUD_FORMATION_TEMPLATE_URL_CLOUD_CONNECTORS + ) + ?.replace(TEMPLATE_URL_ACCOUNT_TYPE_ENV_VAR, accountType) + ?.replace(TEMPLATE_URL_ELASTIC_RESOURCE_ID_ENV_VAR, elasticResourceId); +}; + +/** + * Update AWS cloud connector credentials in package policy + */ +export const updatePolicyWithAwsCloudConnectorCredentials = ( + packagePolicy: NewPackagePolicy, + input: NewPackagePolicyInput, + credentials: Record +): NewPackagePolicy => { + if (!credentials) return packagePolicy; + + const updatedPolicy = { ...packagePolicy }; + + if (!updatedPolicy.inputs) { + updatedPolicy.inputs = []; + } + + if (!input.streams[0].vars) return updatedPolicy; + + const updatedVars = { ...input.streams[0].vars }; + + // Update role_arn + if (credentials[CLOUD_CONNECTOR_FIELD_NAMES.ROLE_ARN]) { + updatedVars[CLOUD_CONNECTOR_FIELD_NAMES.ROLE_ARN].value = + credentials[CLOUD_CONNECTOR_FIELD_NAMES.ROLE_ARN]; + } + // Update external_id + if (credentials[CLOUD_CONNECTOR_FIELD_NAMES.EXTERNAL_ID]) { + updatedVars[CLOUD_CONNECTOR_FIELD_NAMES.EXTERNAL_ID].value = + credentials[CLOUD_CONNECTOR_FIELD_NAMES.EXTERNAL_ID]; + } + // Update aws.role_arn + if (credentials[CLOUD_CONNECTOR_FIELD_NAMES.AWS_ROLE_ARN]) { + updatedVars[CLOUD_CONNECTOR_FIELD_NAMES.AWS_ROLE_ARN].value = + credentials[CLOUD_CONNECTOR_FIELD_NAMES.AWS_ROLE_ARN]; + } + // Update aws.credentials.external_id + if (credentials[CLOUD_CONNECTOR_FIELD_NAMES.AWS_EXTERNAL_ID]) { + updatedVars[CLOUD_CONNECTOR_FIELD_NAMES.AWS_EXTERNAL_ID].value = + credentials[CLOUD_CONNECTOR_FIELD_NAMES.AWS_EXTERNAL_ID]; + } + + updatedPolicy.inputs = [ + ...updatedPolicy.inputs + .map((i) => { + if (i.enabled && i.streams[0].enabled) { + return { + ...i, + streams: i.streams.map((s) => { + if (s.enabled) { + return { + ...s, + vars: updatedVars, + }; + } + return s; // Return the original stream if not enabled + }), + }; + } + return i; // Return the original input if not enabled + }) + .filter(Boolean), // Filter out undefined values + ]; + + return updatedPolicy; +}; + +/** + * Updates input variables with current credentials + * @param inputVars - The original input variables + * @param credentials - The current credentials to apply + * @returns Updated input variables with credentials applied + */ +export const updateInputVarsWithCredentials = ( + inputVars: PackagePolicyVars | undefined, + credentials: CloudConnectorCredentials | undefined, + isNewCredentials: boolean = false +): PackagePolicyVars | undefined => { + if (!inputVars) return inputVars; + + const updatedInputVars: PackagePolicyVars = { ...inputVars }; + + // Update role_arn fields + if (credentials?.roleArn !== undefined) { + if (updatedInputVars.role_arn) { + updatedInputVars.role_arn.value = credentials.roleArn; + } + if (updatedInputVars['aws.role_arn']) { + updatedInputVars['aws.role_arn'].value = credentials.roleArn; + } + } else { + // Clear role_arn fields when roleArn is undefined + if (updatedInputVars.role_arn) { + updatedInputVars.role_arn = { value: undefined }; + } + if (updatedInputVars['aws.role_arn']) { + updatedInputVars['aws.role_arn'] = { value: undefined }; + } + } + + // Update external_id fields + if (credentials?.externalId !== undefined) { + if (updatedInputVars.external_id) { + updatedInputVars.external_id = isNewCredentials + ? { value: credentials.externalId } + : credentials.externalId; + } + if (updatedInputVars['aws.credentials.external_id']) { + updatedInputVars['aws.credentials.external_id'] = { value: credentials.externalId }; + } + } else { + // Clear external_id fields when externalId is undefined + if (updatedInputVars.external_id) { + updatedInputVars.external_id = { value: undefined }; + } + if (updatedInputVars['aws.credentials.external_id']) { + updatedInputVars['aws.credentials.external_id'] = { value: undefined }; + } + } + + return updatedInputVars; +}; + +/** + * Updates policy inputs with new variables + * @param updatedPolicy - The policy to update + * @param inputVars - The variables to apply to the policy + * @returns Updated policy with new inputs + */ +export const updatePolicyInputs = ( + updatedPolicy: NewPackagePolicy, + inputVars: PackagePolicyVars +): NewPackagePolicy => { + if (!updatedPolicy.inputs || updatedPolicy.inputs.length === 0 || !inputVars) { + return updatedPolicy; + } + + const updatedInputs = updatedPolicy.inputs.map((policyInput) => { + if (policyInput.enabled && policyInput.streams && policyInput.streams.length > 0) { + const updatedStreams = policyInput.streams.map((stream) => { + if (stream.enabled) { + return { + ...stream, + vars: inputVars, + }; + } + return stream; + }); + + return { + ...policyInput, + streams: updatedStreams, + }; + } + return policyInput; + }); + + return { + ...updatedPolicy, + inputs: updatedInputs, + }; +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_setup.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_setup.tsx index 7551f27e4ca6b..6e2fb9497a9ca 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_setup.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_setup.tsx @@ -256,7 +256,7 @@ const CloudIntegrationSetup = memo( isEditPage={isEditPage} setupTechnology={setupTechnology} hasInvalidRequiredVars={hasInvalidRequiredVars} - showCloudConnectors={showCloudConnectors} + showCloudConnectors={true} cloud={cloud} /> )}