diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json
index 1dd90a0ce138a..d88e58e5eeee1 100644
--- a/oas_docs/bundle.json
+++ b/oas_docs/bundle.json
@@ -25832,6 +25832,200 @@
"x-state": "Technical Preview; added in 9.2.0"
}
},
+ "/api/fleet/cloud_connectors/{cloudConnectorId}/usage": {
+ "get": {
+ "description": "[Required authorization] Route required privileges: fleet-agent-policies-read OR integrations-read.",
+ "operationId": "get-fleet-cloud-connectors-cloudconnectorid-usage",
+ "parameters": [
+ {
+ "description": "The unique identifier of the cloud connector.",
+ "in": "path",
+ "name": "cloudConnectorId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "description": "The page number for pagination.",
+ "in": "query",
+ "name": "page",
+ "required": false,
+ "schema": {
+ "minimum": 1,
+ "type": "number"
+ }
+ },
+ {
+ "description": "The number of items per page.",
+ "in": "query",
+ "name": "perPage",
+ "required": false,
+ "schema": {
+ "minimum": 1,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "examples": {
+ "getCloudConnectorUsageResponseExample": {
+ "description": "Example response showing package policies using the cloud connector",
+ "value": {
+ "items": [
+ {
+ "created_at": "2025-01-16T09:00:00.000Z",
+ "id": "package-policy-1",
+ "name": "CSPM AWS Policy",
+ "package": {
+ "name": "cloud_security_posture",
+ "title": "Cloud Security Posture Management",
+ "version": "3.1.1"
+ },
+ "policy_ids": [
+ "policy-id-123",
+ "policy-id-456"
+ ],
+ "updated_at": "2025-01-16T09:00:00.000Z"
+ }
+ ],
+ "page": 1,
+ "perPage": 20,
+ "total": 2
+ }
+ }
+ },
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "package": {
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "title",
+ "version"
+ ],
+ "type": "object"
+ },
+ "policy_ids": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "policy_ids",
+ "created_at",
+ "updated_at"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "page": {
+ "type": "number"
+ },
+ "perPage": {
+ "type": "number"
+ },
+ "total": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "items",
+ "total",
+ "page",
+ "perPage"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "description": "OK: A successful request."
+ },
+ "400": {
+ "content": {
+ "application/json": {
+ "examples": {
+ "genericErrorResponseExample": {
+ "description": "Example of a generic error response",
+ "value": {
+ "error": "Bad Request",
+ "message": "Cloud connector not found",
+ "statusCode": 400
+ }
+ }
+ },
+ "schema": {
+ "additionalProperties": false,
+ "description": "Generic Error",
+ "properties": {
+ "attributes": {},
+ "error": {
+ "type": "string"
+ },
+ "errorType": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "message",
+ "attributes"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "description": "A bad request."
+ }
+ },
+ "summary": "Get cloud connector usage (package policies using the connector)",
+ "tags": [
+ "Fleet cloud connectors"
+ ],
+ "x-state": "Technical Preview; added in 9.2.0"
+ }
+ },
"/api/fleet/data_streams": {
"get": {
"description": "[Required authorization] Route required privileges: fleet-agents-all AND fleet-agent-policies-all AND fleet-settings-all.",
diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json
index fca3d99db9b79..c5d14ed18e987 100644
--- a/oas_docs/bundle.serverless.json
+++ b/oas_docs/bundle.serverless.json
@@ -25832,6 +25832,200 @@
"x-state": "Technical Preview"
}
},
+ "/api/fleet/cloud_connectors/{cloudConnectorId}/usage": {
+ "get": {
+ "description": "[Required authorization] Route required privileges: fleet-agent-policies-read OR integrations-read.",
+ "operationId": "get-fleet-cloud-connectors-cloudconnectorid-usage",
+ "parameters": [
+ {
+ "description": "The unique identifier of the cloud connector.",
+ "in": "path",
+ "name": "cloudConnectorId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "description": "The page number for pagination.",
+ "in": "query",
+ "name": "page",
+ "required": false,
+ "schema": {
+ "minimum": 1,
+ "type": "number"
+ }
+ },
+ {
+ "description": "The number of items per page.",
+ "in": "query",
+ "name": "perPage",
+ "required": false,
+ "schema": {
+ "minimum": 1,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "examples": {
+ "getCloudConnectorUsageResponseExample": {
+ "description": "Example response showing package policies using the cloud connector",
+ "value": {
+ "items": [
+ {
+ "created_at": "2025-01-16T09:00:00.000Z",
+ "id": "package-policy-1",
+ "name": "CSPM AWS Policy",
+ "package": {
+ "name": "cloud_security_posture",
+ "title": "Cloud Security Posture Management",
+ "version": "3.1.1"
+ },
+ "policy_ids": [
+ "policy-id-123",
+ "policy-id-456"
+ ],
+ "updated_at": "2025-01-16T09:00:00.000Z"
+ }
+ ],
+ "page": 1,
+ "perPage": 20,
+ "total": 2
+ }
+ }
+ },
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "package": {
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "title",
+ "version"
+ ],
+ "type": "object"
+ },
+ "policy_ids": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "policy_ids",
+ "created_at",
+ "updated_at"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "page": {
+ "type": "number"
+ },
+ "perPage": {
+ "type": "number"
+ },
+ "total": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "items",
+ "total",
+ "page",
+ "perPage"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "description": "OK: A successful request."
+ },
+ "400": {
+ "content": {
+ "application/json": {
+ "examples": {
+ "genericErrorResponseExample": {
+ "description": "Example of a generic error response",
+ "value": {
+ "error": "Bad Request",
+ "message": "Cloud connector not found",
+ "statusCode": 400
+ }
+ }
+ },
+ "schema": {
+ "additionalProperties": false,
+ "description": "Generic Error",
+ "properties": {
+ "attributes": {},
+ "error": {
+ "type": "string"
+ },
+ "errorType": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "message",
+ "attributes"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "description": "A bad request."
+ }
+ },
+ "summary": "Get cloud connector usage (package policies using the connector)",
+ "tags": [
+ "Fleet cloud connectors"
+ ],
+ "x-state": "Technical Preview"
+ }
+ },
"/api/fleet/data_streams": {
"get": {
"description": "[Required authorization] Route required privileges: fleet-agents-all AND fleet-agent-policies-all AND fleet-settings-all.",
diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml
index 841d9317bb151..e0dd854142e02 100644
--- a/oas_docs/output/kibana.serverless.yaml
+++ b/oas_docs/output/kibana.serverless.yaml
@@ -31850,6 +31850,150 @@ paths:
x-metaTags:
- content: Kibana, Elastic Cloud Serverless
name: product_name
+ /api/fleet/cloud_connectors/{cloudConnectorId}/usage:
+ get:
+ description: |-
+ **Spaces method and path for this operation:**
+
+
get /s/{space_id}/api/fleet/cloud_connectors/{cloudConnectorId}/usage
+
+ Refer to [Spaces](https://www.elastic.co/docs/deploy-manage/manage-spaces) for more information.
+
+ [Required authorization] Route required privileges: fleet-agent-policies-read OR integrations-read.
+ operationId: get-fleet-cloud-connectors-cloudconnectorid-usage
+ parameters:
+ - description: The unique identifier of the cloud connector.
+ in: path
+ name: cloudConnectorId
+ required: true
+ schema:
+ type: string
+ - description: The page number for pagination.
+ in: query
+ name: page
+ required: false
+ schema:
+ minimum: 1
+ type: number
+ - description: The number of items per page.
+ in: query
+ name: perPage
+ required: false
+ schema:
+ minimum: 1
+ type: number
+ responses:
+ '200':
+ content:
+ application/json:
+ examples:
+ getCloudConnectorUsageResponseExample:
+ description: Example response showing package policies using the cloud connector
+ value:
+ items:
+ - created_at: '2025-01-16T09:00:00.000Z'
+ id: package-policy-1
+ name: CSPM AWS Policy
+ package:
+ name: cloud_security_posture
+ title: Cloud Security Posture Management
+ version: 3.1.1
+ policy_ids:
+ - policy-id-123
+ - policy-id-456
+ updated_at: '2025-01-16T09:00:00.000Z'
+ page: 1
+ perPage: 20
+ total: 2
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ items:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ created_at:
+ type: string
+ id:
+ type: string
+ name:
+ type: string
+ package:
+ additionalProperties: false
+ type: object
+ properties:
+ name:
+ type: string
+ title:
+ type: string
+ version:
+ type: string
+ required:
+ - name
+ - title
+ - version
+ policy_ids:
+ items:
+ type: string
+ type: array
+ updated_at:
+ type: string
+ required:
+ - id
+ - name
+ - policy_ids
+ - created_at
+ - updated_at
+ type: array
+ page:
+ type: number
+ perPage:
+ type: number
+ total:
+ type: number
+ required:
+ - items
+ - total
+ - page
+ - perPage
+ description: 'OK: A successful request.'
+ '400':
+ content:
+ application/json:
+ examples:
+ genericErrorResponseExample:
+ description: Example of a generic error response
+ value:
+ error: Bad Request
+ message: Cloud connector not found
+ statusCode: 400
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ attributes: {}
+ error:
+ type: string
+ errorType:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ - attributes
+ description: A bad request.
+ summary: Get cloud connector usage (package policies using the connector)
+ tags:
+ - Fleet cloud connectors
+ x-state: Technical Preview
+ x-metaTags:
+ - content: Kibana, Elastic Cloud Serverless
+ name: product_name
/api/fleet/data_streams:
get:
description: |-
diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml
index 3f4cbb93315eb..8e01465657dd2 100644
--- a/oas_docs/output/kibana.yaml
+++ b/oas_docs/output/kibana.yaml
@@ -34409,6 +34409,150 @@ paths:
x-metaTags:
- content: Kibana
name: product_name
+ /api/fleet/cloud_connectors/{cloudConnectorId}/usage:
+ get:
+ description: |-
+ **Spaces method and path for this operation:**
+
+ get /s/{space_id}/api/fleet/cloud_connectors/{cloudConnectorId}/usage
+
+ Refer to [Spaces](https://www.elastic.co/docs/deploy-manage/manage-spaces) for more information.
+
+ [Required authorization] Route required privileges: fleet-agent-policies-read OR integrations-read.
+ operationId: get-fleet-cloud-connectors-cloudconnectorid-usage
+ parameters:
+ - description: The unique identifier of the cloud connector.
+ in: path
+ name: cloudConnectorId
+ required: true
+ schema:
+ type: string
+ - description: The page number for pagination.
+ in: query
+ name: page
+ required: false
+ schema:
+ minimum: 1
+ type: number
+ - description: The number of items per page.
+ in: query
+ name: perPage
+ required: false
+ schema:
+ minimum: 1
+ type: number
+ responses:
+ '200':
+ content:
+ application/json:
+ examples:
+ getCloudConnectorUsageResponseExample:
+ description: Example response showing package policies using the cloud connector
+ value:
+ items:
+ - created_at: '2025-01-16T09:00:00.000Z'
+ id: package-policy-1
+ name: CSPM AWS Policy
+ package:
+ name: cloud_security_posture
+ title: Cloud Security Posture Management
+ version: 3.1.1
+ policy_ids:
+ - policy-id-123
+ - policy-id-456
+ updated_at: '2025-01-16T09:00:00.000Z'
+ page: 1
+ perPage: 20
+ total: 2
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ items:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ created_at:
+ type: string
+ id:
+ type: string
+ name:
+ type: string
+ package:
+ additionalProperties: false
+ type: object
+ properties:
+ name:
+ type: string
+ title:
+ type: string
+ version:
+ type: string
+ required:
+ - name
+ - title
+ - version
+ policy_ids:
+ items:
+ type: string
+ type: array
+ updated_at:
+ type: string
+ required:
+ - id
+ - name
+ - policy_ids
+ - created_at
+ - updated_at
+ type: array
+ page:
+ type: number
+ perPage:
+ type: number
+ total:
+ type: number
+ required:
+ - items
+ - total
+ - page
+ - perPage
+ description: 'OK: A successful request.'
+ '400':
+ content:
+ application/json:
+ examples:
+ genericErrorResponseExample:
+ description: Example of a generic error response
+ value:
+ error: Bad Request
+ message: Cloud connector not found
+ statusCode: 400
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ attributes: {}
+ error:
+ type: string
+ errorType:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ - attributes
+ description: A bad request.
+ summary: Get cloud connector usage (package policies using the connector)
+ tags:
+ - Fleet cloud connectors
+ x-state: Technical Preview; added in 9.2.0
+ x-metaTags:
+ - content: Kibana
+ name: product_name
/api/fleet/data_streams:
get:
description: |-
diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts
index 4a60b66613aa5..fef4412432e71 100644
--- a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts
+++ b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts
@@ -106,6 +106,7 @@ export const CLOUD_CONNECTOR_API_ROUTES = {
CREATE_PATTERN: `${CLOUD_CONNECTOR_API_ROOT}`,
UPDATE_PATTERN: `${CLOUD_CONNECTOR_API_ROOT}/{cloudConnectorId}`,
DELETE_PATTERN: `${CLOUD_CONNECTOR_API_ROOT}/{cloudConnectorId}`,
+ USAGE_PATTERN: `${CLOUD_CONNECTOR_API_ROOT}/{cloudConnectorId}/usage`,
};
// Kubernetes Manifest API routes
diff --git a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/cloud_connector.ts b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/cloud_connector.ts
index 08973f826e153..dff0047b3d98e 100644
--- a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/cloud_connector.ts
+++ b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/cloud_connector.ts
@@ -41,3 +41,23 @@ export interface UpdateCloudConnectorResponse {
export interface DeleteCloudConnectorResponse {
id: string;
}
+
+export interface CloudConnectorUsageItem {
+ id: string;
+ name: string;
+ package?: {
+ name: string;
+ title: string;
+ version: string;
+ };
+ policy_ids: string[];
+ created_at: string;
+ updated_at: string;
+}
+
+export interface GetCloudConnectorUsageResponse {
+ items: CloudConnectorUsageItem[];
+ total: number;
+ page: number;
+ perPage: number;
+}
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 1a1063cbaa365..5f50dc17bbe60 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
@@ -7,7 +7,8 @@
import type { TypeOf } from '@kbn/config-schema';
-import { cloudConnectorService } from '../../services';
+import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../common/constants';
+import { cloudConnectorService, packagePolicyService } from '../../services';
import type { FleetRequestHandler } from '../../types';
import { appContextService } from '../../services/app_context';
import type {
@@ -18,6 +19,8 @@ import type {
DeleteCloudConnectorResponse,
UpdateCloudConnectorRequest,
CreateCloudConnectorRequest,
+ GetCloudConnectorUsageResponse,
+ CloudConnectorUsageItem,
} from '../../../common/types/rest_spec/cloud_connector';
import type {
CreateCloudConnectorRequestSchema,
@@ -25,6 +28,7 @@ import type {
GetCloudConnectorsRequestSchema,
UpdateCloudConnectorRequestSchema,
DeleteCloudConnectorRequestSchema,
+ GetCloudConnectorUsageRequestSchema,
} from '../../types/rest_spec/cloud_connector';
export const createCloudConnectorHandler: FleetRequestHandler<
@@ -193,3 +197,78 @@ export const deleteCloudConnectorHandler: FleetRequestHandler<
});
}
};
+
+export const getCloudConnectorUsageHandler: FleetRequestHandler<
+ TypeOf,
+ TypeOf
+> = async (context, request, response) => {
+ const fleetContext = await context.fleet;
+ const { internalSoClient } = fleetContext;
+ const cloudConnectorId = request.params.cloudConnectorId;
+ const page = request.query?.page || 1;
+ const perPage = request.query?.perPage || 10;
+ const logger = appContextService
+ .getLogger()
+ .get('CloudConnectorService getCloudConnectorUsageHandler');
+
+ try {
+ logger.info(
+ `Getting usage for cloud connector ${cloudConnectorId} (page: ${page}, perPage: ${perPage})`
+ );
+
+ // First, verify the cloud connector exists
+ await cloudConnectorService.getById(internalSoClient, cloudConnectorId);
+
+ // Query package policies that use this cloud connector with pagination
+ logger.debug(
+ `Querying package policies with kuery: ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.cloud_connector_id:"${cloudConnectorId}"`
+ );
+
+ const result = await packagePolicyService.list(internalSoClient, {
+ page,
+ perPage,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.cloud_connector_id:"${cloudConnectorId}"`,
+ });
+
+ logger.debug(`Found ${result?.total || 0} total package policies using cloud connector`);
+
+ const usageItems: CloudConnectorUsageItem[] = (result?.items || []).map((policy) => ({
+ id: policy.id,
+ name: policy.name,
+ package: policy.package
+ ? {
+ name: policy.package.name,
+ title: policy.package.title,
+ version: policy.package.version,
+ }
+ : undefined,
+ policy_ids: policy.policy_ids,
+ created_at: policy.created_at,
+ updated_at: policy.updated_at,
+ }));
+
+ logger.info(
+ `Successfully retrieved usage for cloud connector ${cloudConnectorId}: ${
+ usageItems.length
+ } of ${result?.total || 0} policies`
+ );
+ const body: GetCloudConnectorUsageResponse = {
+ items: usageItems,
+ total: result?.total || 0,
+ page,
+ perPage,
+ };
+ return response.ok({ body });
+ } catch (error) {
+ logger.error(
+ `Failed to get usage for cloud connector ${cloudConnectorId}: ${error.message}`,
+ error
+ );
+ return response.customError({
+ statusCode: 400,
+ body: {
+ message: error.message || 'Failed to get cloud connector usage',
+ },
+ });
+ }
+};
diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/index.ts b/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/index.ts
index 3938bc4a58c6d..cb95a7ad53925 100644
--- a/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/index.ts
+++ b/x-pack/platform/plugins/shared/fleet/server/routes/cloud_connector/index.ts
@@ -22,6 +22,8 @@ import {
UpdateCloudConnectorResponseSchema,
DeleteCloudConnectorRequestSchema,
DeleteCloudConnectorResponseSchema,
+ GetCloudConnectorUsageRequestSchema,
+ GetCloudConnectorUsageResponseSchema,
} from '../../types/rest_spec/cloud_connector';
import {
@@ -30,6 +32,7 @@ import {
getCloudConnectorHandler,
updateCloudConnectorHandler,
deleteCloudConnectorHandler,
+ getCloudConnectorUsageHandler,
} from './handlers';
export const registerRoutes = (router: FleetAuthzRouter) => {
@@ -257,4 +260,102 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
},
deleteCloudConnectorHandler
);
+
+ // GET /api/fleet/cloud_connectors/{cloudConnectorId}/usage
+ router.versioned
+ .get({
+ path: CLOUD_CONNECTOR_API_ROUTES.USAGE_PATTERN,
+ security: {
+ authz: {
+ requiredPrivileges: [
+ {
+ anyRequired: [
+ FLEET_API_PRIVILEGES.AGENT_POLICIES.READ,
+ FLEET_API_PRIVILEGES.INTEGRATIONS.READ,
+ ],
+ },
+ ],
+ },
+ },
+ summary: 'Get cloud connector usage (package policies using the connector)',
+ options: {
+ tags: ['oas-tag:Fleet cloud connectors'],
+ availability: {
+ since: '9.2.0',
+ stability: 'experimental',
+ },
+ },
+ })
+ .addVersion(
+ {
+ version: API_VERSIONS.public.v1,
+ options: {
+ oasOperationObject: () => ({
+ responses: {
+ '200': {
+ content: {
+ 'application/json': {
+ examples: {
+ getCloudConnectorUsageResponseExample: {
+ description:
+ 'Example response showing package policies using the cloud connector',
+ value: {
+ items: [
+ {
+ id: 'package-policy-1',
+ name: 'CSPM AWS Policy',
+ package: {
+ name: 'cloud_security_posture',
+ title: 'Cloud Security Posture Management',
+ version: '3.1.1',
+ },
+ policy_ids: ['policy-id-123', 'policy-id-456'],
+ created_at: '2025-01-16T09:00:00.000Z',
+ updated_at: '2025-01-16T09:00:00.000Z',
+ },
+ ],
+ total: 2,
+ page: 1,
+ perPage: 20,
+ },
+ },
+ },
+ },
+ },
+ },
+ '400': {
+ content: {
+ 'application/json': {
+ examples: {
+ genericErrorResponseExample: {
+ description: 'Example of a generic error response',
+ value: {
+ statusCode: 400,
+ error: 'Bad Request',
+ message: 'Cloud connector not found',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ },
+ validate: {
+ request: GetCloudConnectorUsageRequestSchema,
+ response: {
+ 200: {
+ body: () => GetCloudConnectorUsageResponseSchema,
+ description: 'OK: A successful request.',
+ },
+ 400: {
+ body: genericErrorResponse,
+ description: 'A bad request.',
+ },
+ },
+ },
+ },
+ getCloudConnectorUsageHandler
+ );
};
diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/cloud_connector.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/cloud_connector.ts
index a5c5c7a628035..6755a92dbd5c2 100644
--- a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/cloud_connector.ts
+++ b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/cloud_connector.ts
@@ -184,3 +184,47 @@ export const UpdateCloudConnectorResponseSchema = schema.object({
updated_at: schema.string(),
}),
});
+
+export const GetCloudConnectorUsageRequestSchema = {
+ params: schema.object({
+ cloudConnectorId: schema.string({
+ meta: { description: 'The unique identifier of the cloud connector.' },
+ }),
+ }),
+ query: schema.object({
+ page: schema.maybe(
+ schema.number({
+ min: 1,
+ meta: { description: 'The page number for pagination.' },
+ })
+ ),
+ perPage: schema.maybe(
+ schema.number({
+ min: 1,
+ meta: { description: 'The number of items per page.' },
+ })
+ ),
+ }),
+};
+
+export const GetCloudConnectorUsageResponseSchema = schema.object({
+ items: schema.arrayOf(
+ schema.object({
+ id: schema.string(),
+ name: schema.string(),
+ package: schema.maybe(
+ schema.object({
+ name: schema.string(),
+ title: schema.string(),
+ version: schema.string(),
+ })
+ ),
+ policy_ids: schema.arrayOf(schema.string()),
+ created_at: schema.string(),
+ updated_at: schema.string(),
+ })
+ ),
+ total: schema.number(),
+ page: schema.number(),
+ perPage: schema.number(),
+});
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/common/test_subjects.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/common/test_subjects.ts
index 44f2216ab8141..222c7425e22de 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/common/test_subjects.ts
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/common/test_subjects.ts
@@ -80,3 +80,21 @@ export const NAMESPACE_INPUT_TEST_SUBJ = 'namespaceInputTestId';
// Cloud Connector test subjects
export const AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ = 'aws-cloud-connector-super-select';
export const AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ = 'azure-cloud-connector-super-select';
+export const CLOUD_CONNECTOR_EDIT_ICON_TEST_SUBJ = 'cloudConnectorEditIcon';
+export const getCloudConnectorEditIconTestSubj = (connectorId: string) =>
+ `${CLOUD_CONNECTOR_EDIT_ICON_TEST_SUBJ}-${connectorId}`;
+
+export const CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS = {
+ FLYOUT: 'cloudConnectorPoliciesFlyout',
+ CLOSE_BUTTON: 'euiFlyoutCloseButton',
+ TITLE: 'cloudConnectorPoliciesFlyoutTitle',
+ IDENTIFIER_TEXT: 'cloudConnectorIdentifierText',
+ COPY_IDENTIFIER_BUTTON: 'cloudConnectorCopyIdentifier',
+ NAME_INPUT: 'cloudConnectorNameInput',
+ SAVE_NAME_BUTTON: 'cloudConnectorSaveNameButton',
+ USAGE_COUNT_TEXT: 'cloudConnectorUsageCountText',
+ POLICIES_TABLE: 'cloudConnectorPoliciesTable',
+ POLICY_LINK: 'cloudConnectorPolicyLink',
+ EMPTY_STATE: 'cloudConnectorPoliciesEmptyState',
+ ERROR_STATE: 'cloudConnectorPoliciesErrorState',
+};
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_policies_flyout/index.test.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_policies_flyout/index.test.tsx
new file mode 100644
index 0000000000000..696cc463744d4
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_policies_flyout/index.test.tsx
@@ -0,0 +1,478 @@
+/*
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { I18nProvider } from '@kbn/i18n-react';
+import { CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS } from '@kbn/cloud-security-posture-common';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import type { UseQueryResult } from '@kbn/react-query';
+import { QueryClient, QueryClientProvider } from '@kbn/react-query';
+import { CloudConnectorPoliciesFlyout } from '.';
+import type { CloudConnectorUsageItem } from '../hooks/use_cloud_connector_usage';
+import { useCloudConnectorUsage } from '../hooks/use_cloud_connector_usage';
+import { useUpdateCloudConnector } from '../hooks/use_update_cloud_connector';
+
+jest.mock('@kbn/kibana-react-plugin/public');
+jest.mock('../hooks/use_cloud_connector_usage');
+jest.mock('../hooks/use_update_cloud_connector');
+
+const mockUseKibana = useKibana as jest.MockedFunction;
+const mockUseCloudConnectorUsage = useCloudConnectorUsage as jest.MockedFunction<
+ typeof useCloudConnectorUsage
+>;
+const mockUseUpdateCloudConnector = useUpdateCloudConnector as jest.MockedFunction<
+ typeof useUpdateCloudConnector
+>;
+
+describe('CloudConnectorPoliciesFlyout', () => {
+ let queryClient: QueryClient;
+ const mockOnClose = jest.fn();
+ const mockNavigateToApp = jest.fn();
+
+ const defaultProps = {
+ cloudConnectorId: 'connector-123',
+ cloudConnectorName: 'Test Connector',
+ cloudConnectorVars: {
+ role_arn: { value: 'arn:aws:iam::123456789012:role/TestRole' },
+ external_id: { value: { isSecretRef: true, id: 'secret-ref-id-123' } },
+ },
+ provider: 'aws' as const,
+ onClose: mockOnClose,
+ };
+
+ const mockUsageData: CloudConnectorUsageItem[] = [
+ {
+ id: 'policy-1',
+ name: 'Test Policy 1',
+ package: {
+ name: 'cloud_security_posture',
+ title: 'Cloud Security Posture',
+ version: '1.0.0',
+ },
+ policy_ids: ['agent-policy-1'],
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-02T00:00:00Z',
+ },
+ ];
+
+ beforeEach(() => {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ mockUseKibana.mockReturnValue({
+ services: {
+ application: {
+ navigateToApp: mockNavigateToApp,
+ },
+ },
+ } as unknown as ReturnType);
+
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: { items: mockUsageData, total: mockUsageData.length, page: 1, perPage: 10 },
+ isLoading: false,
+ error: null,
+ } as unknown as UseQueryResult<{ items: CloudConnectorUsageItem[]; total: number; page: number; perPage: number }>);
+
+ const mockMutate = jest.fn();
+ mockUseUpdateCloudConnector.mockReturnValue({
+ mutate: mockMutate,
+ isLoading: false,
+ } as unknown as ReturnType);
+
+ mockOnClose.mockClear();
+ mockNavigateToApp.mockClear();
+ });
+
+ afterEach(() => {
+ queryClient.clear();
+ });
+
+ const renderFlyout = (props = {}) => {
+ return render(
+
+
+
+
+
+ );
+ };
+
+ it('should render flyout with connector name and ARN', () => {
+ renderFlyout();
+
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.TITLE)
+ ).toHaveTextContent('Test Connector');
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.IDENTIFIER_TEXT)
+ ).toHaveTextContent('Role ARN: arn:aws:iam::123456789012:role/TestRole');
+ });
+
+ it('should render usage table with policies', async () => {
+ renderFlyout();
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICIES_TABLE)
+ ).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Test Policy 1')).toBeInTheDocument();
+ expect(screen.getByText('Cloud Security Posture')).toBeInTheDocument();
+ });
+
+ it('should show empty state when no policies use the connector', () => {
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: { items: [], total: 0, page: 1, perPage: 10 },
+ isLoading: false,
+ error: null,
+ } as unknown as UseQueryResult<{ items: CloudConnectorUsageItem[]; total: number; page: number; perPage: number }>);
+
+ renderFlyout();
+
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.EMPTY_STATE)
+ ).toBeInTheDocument();
+ expect(screen.getByText('No integrations using this cloud connector')).toBeInTheDocument();
+ });
+
+ it('should show loading state', () => {
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ error: null,
+ } as unknown as UseQueryResult<{ items: CloudConnectorUsageItem[]; total: number; page: number; perPage: number }>);
+
+ renderFlyout();
+
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICIES_TABLE)
+ ).toHaveClass('euiBasicTable-loading');
+ });
+
+ it('should show error state', () => {
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ error: new Error('Failed to fetch'),
+ } as unknown as UseQueryResult<{ items: CloudConnectorUsageItem[]; total: number; page: number; perPage: number }>);
+
+ renderFlyout();
+
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.ERROR_STATE)
+ ).toBeInTheDocument();
+ expect(screen.getByText('Failed to load policies')).toBeInTheDocument();
+ });
+
+ it('should enable save button when name is changed', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT);
+ const saveButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.SAVE_NAME_BUTTON
+ );
+
+ expect(saveButton).toBeDisabled();
+
+ await user.clear(nameInput);
+ await user.type(nameInput, 'New Name');
+
+ expect(saveButton).toBeEnabled();
+ });
+
+ it('should call mutate when save button is clicked', async () => {
+ const user = userEvent.setup();
+ const mockMutate = jest.fn();
+ mockUseUpdateCloudConnector.mockReturnValue({
+ mutate: mockMutate,
+ isLoading: false,
+ } as unknown as ReturnType);
+
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT);
+ const saveButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.SAVE_NAME_BUTTON
+ );
+
+ await user.clear(nameInput);
+ await user.type(nameInput, 'New Name');
+ await user.click(saveButton);
+
+ expect(mockMutate).toHaveBeenCalledWith({ name: 'New Name' });
+ });
+
+ it('should navigate to policy when clicking policy name', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Policy 1')).toBeInTheDocument();
+ });
+
+ const policyLink = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICY_LINK
+ );
+ await user.click(policyLink);
+
+ expect(mockNavigateToApp).toHaveBeenCalledWith('integrations', {
+ path: '/edit-integration/policy-1',
+ });
+ });
+
+ it('should close flyout when onClose is called', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const closeButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.CLOSE_BUTTON
+ );
+ await user.click(closeButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('should display Azure subscription ID for Azure connector', () => {
+ renderFlyout({
+ provider: 'azure',
+ cloudConnectorVars: {
+ tenant_id: { value: 'tenant-123' },
+ azure_credentials_cloud_connector_id: { value: 'subscription-123' },
+ },
+ });
+
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.IDENTIFIER_TEXT)
+ ).toHaveTextContent('Cloud Connector ID: subscription-123');
+ });
+
+ describe('pagination', () => {
+ it('should call useCloudConnectorUsage with initial pagination parameters', () => {
+ renderFlyout();
+
+ expect(mockUseCloudConnectorUsage).toHaveBeenCalledWith('connector-123', 1, 10);
+ });
+
+ it('should display pagination controls when there are multiple pages', async () => {
+ const manyPolicies: CloudConnectorUsageItem[] = Array.from({ length: 15 }, (_, i) => ({
+ id: `policy-${i + 1}`,
+ name: `Test Policy ${i + 1}`,
+ package: {
+ name: 'cloud_security_posture',
+ title: 'Cloud Security Posture',
+ version: '1.0.0',
+ },
+ policy_ids: [`agent-policy-${i + 1}`],
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-02T00:00:00Z',
+ }));
+
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: { items: manyPolicies.slice(0, 10), total: 15, page: 1, perPage: 10 },
+ isLoading: false,
+ error: null,
+ } as unknown as UseQueryResult<{
+ items: CloudConnectorUsageItem[];
+ total: number;
+ page: number;
+ perPage: number;
+ }>);
+
+ renderFlyout();
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICIES_TABLE)
+ ).toBeInTheDocument();
+ });
+
+ // EuiBasicTable renders pagination when totalItemCount > pageSize
+ expect(screen.getByText('Rows per page: 10')).toBeInTheDocument();
+ });
+
+ it('should update pagination when page is changed', async () => {
+ const user = userEvent.setup();
+
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: { items: mockUsageData, total: 25, page: 1, perPage: 10 },
+ isLoading: false,
+ error: null,
+ } as unknown as UseQueryResult<{
+ items: CloudConnectorUsageItem[];
+ total: number;
+ page: number;
+ perPage: number;
+ }>);
+
+ renderFlyout();
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICIES_TABLE)
+ ).toBeInTheDocument();
+ });
+
+ // Click next page button
+ const nextPageButton = screen.getByLabelText('Next page');
+ await user.click(nextPageButton);
+
+ // Verify the hook was called with page 2
+ expect(mockUseCloudConnectorUsage).toHaveBeenLastCalledWith('connector-123', 2, 10);
+ });
+
+ it('should update pagination when page size is changed', async () => {
+ const user = userEvent.setup();
+
+ mockUseCloudConnectorUsage.mockReturnValue({
+ data: { items: mockUsageData, total: 30, page: 1, perPage: 10 },
+ isLoading: false,
+ error: null,
+ } as unknown as UseQueryResult<{
+ items: CloudConnectorUsageItem[];
+ total: number;
+ page: number;
+ perPage: number;
+ }>);
+
+ renderFlyout();
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId(CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICIES_TABLE)
+ ).toBeInTheDocument();
+ });
+
+ // Click on "Rows per page" button and select 25
+ const rowsPerPageButton = screen.getByText('Rows per page: 10');
+ await user.click(rowsPerPageButton);
+
+ const option25 = await screen.findByText('25 rows');
+ await user.click(option25);
+
+ // Verify the hook was called with new page size
+ expect(mockUseCloudConnectorUsage).toHaveBeenLastCalledWith('connector-123', 1, 25);
+ });
+ });
+
+ describe('name validation', () => {
+ it('should show validation error when name exceeds 255 characters', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT
+ );
+
+ await user.clear(nameInput);
+ await user.click(nameInput);
+ await user.paste('a'.repeat(256));
+
+ expect(
+ screen.getByText('Cloud Connector Name must be 255 characters or less')
+ ).toBeInTheDocument();
+ });
+
+ it('should keep save button disabled when name exceeds 255 characters', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT
+ );
+ const saveButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.SAVE_NAME_BUTTON
+ );
+
+ await user.clear(nameInput);
+ await user.click(nameInput);
+ await user.paste('a'.repeat(256));
+
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('should show validation error when name is empty', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT
+ );
+
+ await user.clear(nameInput);
+
+ expect(screen.getByText('Cloud Connector Name is required')).toBeInTheDocument();
+ });
+
+ it('should keep save button disabled when name is empty', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT
+ );
+ const saveButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.SAVE_NAME_BUTTON
+ );
+
+ await user.clear(nameInput);
+
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('should enable save button for valid name that is different from original', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT
+ );
+ const saveButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.SAVE_NAME_BUTTON
+ );
+
+ await user.clear(nameInput);
+ await user.click(nameInput);
+ await user.paste('Valid New Name');
+
+ expect(saveButton).toBeEnabled();
+ expect(screen.queryByText('Cloud Connector Name is required')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('Cloud Connector Name must be 255 characters or less')
+ ).not.toBeInTheDocument();
+ });
+
+ it('should accept name with exactly 255 characters', async () => {
+ const user = userEvent.setup();
+ renderFlyout();
+
+ const nameInput = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.NAME_INPUT
+ );
+ const saveButton = screen.getByTestId(
+ CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.SAVE_NAME_BUTTON
+ );
+
+ await user.clear(nameInput);
+ await user.click(nameInput);
+ await user.paste('a'.repeat(255));
+
+ expect(saveButton).toBeEnabled();
+ expect(
+ screen.queryByText('Cloud Connector Name must be 255 characters or less')
+ ).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_policies_flyout/index.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_policies_flyout/index.tsx
new file mode 100644
index 0000000000000..fc22a229dfefd
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/cloud_connector_policies_flyout/index.tsx
@@ -0,0 +1,386 @@
+/*
+ * 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, useMemo, useCallback } from 'react';
+import {
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiTitle,
+ EuiText,
+ EuiSpacer,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBasicTable,
+ EuiLink,
+ EuiEmptyPrompt,
+ EuiCopy,
+ EuiButtonIcon,
+ useGeneratedHtmlId,
+ type EuiBasicTableColumn,
+ EuiHorizontalRule,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { pagePathGetters } from '@kbn/fleet-plugin/public';
+import type { CloudConnectorVars } from '@kbn/fleet-plugin/common/types';
+import { CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS } from '@kbn/cloud-security-posture-common';
+import type { CloudProviders } from '../types';
+import { useCloudConnectorUsage } from '../hooks/use_cloud_connector_usage';
+import { useUpdateCloudConnector } from '../hooks/use_update_cloud_connector';
+import {
+ isAwsCloudConnectorVars,
+ isAzureCloudConnectorVars,
+ isCloudConnectorNameValid,
+} from '../utils';
+import { CloudConnectorNameField } from '../form/cloud_connector_name_field';
+
+interface CloudConnectorPoliciesFlyoutProps {
+ cloudConnectorId: string;
+ cloudConnectorName: string;
+ cloudConnectorVars: CloudConnectorVars;
+ provider: CloudProviders;
+ onClose: () => void;
+}
+
+export const CloudConnectorPoliciesFlyout: React.FC = ({
+ cloudConnectorId,
+ cloudConnectorName: initialName,
+ cloudConnectorVars,
+ provider,
+ onClose,
+}) => {
+ const { application } = useKibana().services;
+ const flyoutTitleId = useGeneratedHtmlId();
+ const [cloudConnectorName, setCloudConnectorName] = useState(initialName);
+ const [editedName, setEditedName] = useState(initialName);
+ const [isNameValid, setIsNameValid] = useState(() => isCloudConnectorNameValid(initialName));
+ const [pageIndex, setPageIndex] = useState(0);
+ const [pageSize, setPageSize] = useState(10);
+
+ const {
+ data: usageData,
+ isLoading,
+ error,
+ } = useCloudConnectorUsage(
+ cloudConnectorId,
+ pageIndex + 1, // Convert from 0-based to 1-based
+ pageSize
+ );
+
+ const usageItems = usageData?.items || [];
+ const totalItemCount = usageData?.total || 0;
+
+ const { mutate: updateConnector, isLoading: isUpdating } = useUpdateCloudConnector(
+ cloudConnectorId,
+ (updatedConnector) => {
+ setCloudConnectorName(updatedConnector.name);
+ setEditedName(updatedConnector.name);
+ setIsNameValid(true);
+ }
+ );
+
+ // Extract ARN or Subscription ID based on provider
+ const identifier = useMemo(() => {
+ if (isAwsCloudConnectorVars(cloudConnectorVars, provider)) {
+ return cloudConnectorVars.role_arn?.value || '';
+ } else if (isAzureCloudConnectorVars(cloudConnectorVars, provider)) {
+ return cloudConnectorVars.azure_credentials_cloud_connector_id?.value || '';
+ }
+ return '';
+ }, [cloudConnectorVars, provider]);
+
+ const identifierLabel =
+ provider === 'aws'
+ ? i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.roleArnLabel',
+ {
+ defaultMessage: 'Role ARN',
+ }
+ )
+ : i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.cloudConnectorIdLabel',
+ {
+ defaultMessage: 'Cloud Connector ID',
+ }
+ );
+
+ const handleSaveName = () => {
+ if (editedName && editedName !== cloudConnectorName) {
+ updateConnector({ name: editedName });
+ }
+ };
+
+ const handleNameChange = useCallback((name: string, valid: boolean) => {
+ setEditedName(name);
+ setIsNameValid(valid);
+ }, []);
+
+ const isSaveDisabled = !isNameValid || editedName === cloudConnectorName || isUpdating;
+
+ const tableCaption = useMemo(
+ () =>
+ i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.tableCaption',
+ {
+ defaultMessage: 'Integrations using cloud connector {name}',
+ values: { name: cloudConnectorName },
+ }
+ ),
+ [cloudConnectorName]
+ );
+
+ const pagination = useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ totalItemCount,
+ pageSizeOptions: [10, 25, 50],
+ }),
+ [pageIndex, pageSize, totalItemCount]
+ );
+
+ const onTableChange = useCallback(({ page }: { page?: { index: number; size: number } }) => {
+ if (page) {
+ setPageIndex(page.index);
+ setPageSize(page.size);
+ }
+ }, []);
+
+ const handleNavigateToPolicy = useCallback(
+ (packagePolicyId: string) => {
+ // Use integrations app route to ensure cancel navigates back to integrations page
+ const [, path] = pagePathGetters.integration_policy_edit({ packagePolicyId });
+ application?.navigateToApp('integrations', { path });
+ },
+ [application]
+ );
+
+ const columns: Array> = useMemo(
+ () => [
+ {
+ field: 'name',
+ name: i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.nameColumn',
+ {
+ defaultMessage: 'Name',
+ }
+ ),
+ render: (name: string, item) => {
+ return (
+ {
+ e.preventDefault();
+ handleNavigateToPolicy(item.id);
+ }}
+ data-test-subj={CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.POLICY_LINK}
+ >
+ {name}
+
+ );
+ },
+ },
+ {
+ field: 'package',
+ name: i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.integrationTypeColumn',
+ {
+ defaultMessage: 'Integration Type',
+ }
+ ),
+ render: (pkg: (typeof usageItems)[0]['package']) => pkg?.title || pkg?.name || '-',
+ },
+ {
+ field: 'created_at',
+ name: i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.createdColumn',
+ {
+ defaultMessage: 'Created',
+ }
+ ),
+ render: (createdAt: string) => new Date(createdAt).toLocaleDateString(),
+ },
+ {
+ field: 'updated_at',
+ name: i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorPoliciesFlyout.lastUpdatedColumn',
+ {
+ defaultMessage: 'Last Updated',
+ }
+ ),
+ render: (updatedAt: string) => new Date(updatedAt).toLocaleDateString(),
+ },
+ ],
+ [handleNavigateToPolicy]
+ );
+
+ return (
+
+
+
+
+ {cloudConnectorName}
+
+
+
+
+
+
+ {identifierLabel}
+ {': '}
+ {identifier}
+
+
+ {identifier && (
+
+
+ {(copy) => (
+
+ )}
+
+
+ )}
+
+
+
+
+ {/* Edit Name Section */}
+
+
+
+
+
+
+
+
+
+
+ {/* Usage Section */}
+
+
+
+
+
+
+
+
+ {error ? (
+
+
+
+ }
+ body={
+
+
+
+ }
+ data-test-subj={CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.ERROR_STATE}
+ />
+ ) : isLoading ? (
+
+ ) : usageItems.length === 0 ? (
+
+
+
+ }
+ body={
+
+
+
+ }
+ data-test-subj={CLOUD_CONNECTOR_POLICIES_FLYOUT_TEST_SUBJECTS.EMPTY_STATE}
+ />
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_name_field.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_name_field.tsx
index 5247bb085c5e1..10bbc0de804b7 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_name_field.tsx
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_name_field.tsx
@@ -8,45 +8,29 @@
import React from 'react';
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { getCloudConnectorNameError } from '../utils';
interface CloudConnectorNameFieldProps {
value: string;
onChange: (name: string, isValid: boolean, validationError?: string) => void;
disabled?: boolean;
+ 'data-test-subj'?: string;
}
export const CloudConnectorNameField: React.FC = ({
value,
onChange,
disabled = false,
+ 'data-test-subj': dataTestSubj,
}) => {
- // Format validation only
- const validateFormat = (name: string): string | undefined => {
- if (!name || !name.trim())
- return i18n.translate(
- 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorSetup.cloudConnectorNameField.requiredError',
- {
- defaultMessage: 'Cloud Connector Name is required',
- }
- );
- if (name.length > 255)
- return i18n.translate(
- 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorSetup.cloudConnectorNameField.tooLongError',
- {
- defaultMessage: 'Cloud Connector Name must be 255 characters or less',
- }
- );
- return undefined;
- };
-
const handleChange = (e: React.ChangeEvent) => {
const newValue = e.target.value;
- const formatError = validateFormat(newValue);
+ const formatError = getCloudConnectorNameError(newValue);
onChange(newValue, !formatError, formatError);
};
- const error = validateFormat(value);
+ const error = getCloudConnectorNameError(value);
return (
= (
isInvalid={!!error}
disabled={disabled}
fullWidth
+ data-test-subj={dataTestSubj}
/>
);
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.test.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.test.tsx
index 0bb696ddd066e..a7775a9efb335 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.test.tsx
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.test.tsx
@@ -9,298 +9,183 @@ import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { I18nProvider } from '@kbn/i18n-react';
-import type { CloudProvider } from '@kbn/fleet-plugin/public';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { QueryClient, QueryClientProvider } from '@kbn/react-query';
import {
AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ,
- AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ,
+ getCloudConnectorEditIconTestSubj,
} from '@kbn/cloud-security-posture-common';
import { CloudConnectorSelector } from './cloud_connector_selector';
-import type { AwsCloudConnectorCredentials, AzureCloudConnectorCredentials } from '../types';
+import { useGetCloudConnectors } from '../hooks/use_get_cloud_connectors';
-// Mock the useGetCloudConnectors hook
+jest.mock('@kbn/kibana-react-plugin/public');
jest.mock('../hooks/use_get_cloud_connectors');
-interface UseGetCloudConnectorsReturn {
- data:
- | Array<{
- id: string;
- name: string;
- vars: Record;
- }>
- | undefined;
- isLoading: boolean;
-}
-
-const mockUseGetCloudConnectors = jest.requireMock('../hooks/use_get_cloud_connectors')
- .useGetCloudConnectors as jest.MockedFunction<
- (provider?: CloudProvider) => UseGetCloudConnectorsReturn
+const mockUseKibana = useKibana as jest.MockedFunction;
+const mockUseGetCloudConnectors = useGetCloudConnectors as jest.MockedFunction<
+ typeof useGetCloudConnectors
>;
-// Helper to render with I18n provider
-const renderWithIntl = (component: React.ReactElement) => {
- return render({component});
-};
-
-// Mock cloud connector data for AWS
-const mockAwsCloudConnectors = [
- {
- id: 'aws-connector-1',
- name: 'AWS Connector 1',
- vars: {
- role_arn: { value: 'arn:aws:iam::123456789012:role/Role1' },
- external_id: { value: 'external-id-123' },
- },
- },
- {
- id: 'aws-connector-2',
- name: 'AWS Connector 2',
- vars: {
- role_arn: { value: 'arn:aws:iam::123456789012:role/Role2' },
- external_id: { value: 'external-id-456' },
- },
- },
-];
-
-// Mock cloud connector data for Azure
-const mockAzureCloudConnectors = [
- {
- id: 'azure-connector-1',
- name: 'Azure Connector 1',
- vars: {
- tenant_id: { value: 'tenant-123' },
- client_id: { value: 'client-456' },
- azure_credentials_cloud_connector_id: { value: 'azure-cc-789' },
- },
- },
-];
-
describe('CloudConnectorSelector', () => {
+ let queryClient: QueryClient;
const mockSetCredentials = jest.fn();
+ const mockCloudConnectors = [
+ {
+ id: 'connector-1',
+ name: 'AWS Connector 1',
+ cloudProvider: 'aws',
+ vars: {
+ role_arn: { value: 'arn:aws:iam::123456789012:role/Role1' },
+ external_id: { value: 'external-id-1' },
+ },
+ packagePolicyCount: 2,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-02T00:00:00Z',
+ },
+ {
+ id: 'connector-2',
+ name: 'AWS Connector 2',
+ cloudProvider: 'aws',
+ vars: {
+ role_arn: { value: 'arn:aws:iam::123456789012:role/Role2' },
+ external_id: { value: 'external-id-2' },
+ },
+ packagePolicyCount: 1,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-02T00:00:00Z',
+ },
+ ];
+
beforeEach(() => {
- jest.clearAllMocks();
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ mockUseKibana.mockReturnValue({
+ services: {
+ application: {
+ navigateToApp: jest.fn(),
+ },
+ },
+ } as unknown as ReturnType);
+
+ mockUseGetCloudConnectors.mockReturnValue({
+ data: mockCloudConnectors,
+ isLoading: false,
+ error: null,
+ } as unknown as ReturnType);
+
+ mockSetCredentials.mockClear();
});
- describe('AWS Provider', () => {
- const awsProps = {
- provider: 'aws' as CloudProvider,
+ afterEach(() => {
+ queryClient.clear();
+ });
+
+ const renderSelector = (props = {}) => {
+ const defaultProps = {
+ provider: 'aws' as const,
cloudConnectorId: undefined,
- credentials: {
- roleArn: undefined,
- externalId: undefined,
- cloudConnectorId: undefined,
- } as AwsCloudConnectorCredentials,
+ credentials: {},
setCredentials: mockSetCredentials,
+ ...props,
};
- beforeEach(() => {
- mockUseGetCloudConnectors.mockReturnValue({
- data: mockAwsCloudConnectors,
- isLoading: false,
- });
- });
-
- it('displays AWS cloud connectors as options', async () => {
- renderWithIntl();
-
- const select = screen.getByTestId(AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
- await userEvent.click(select);
-
- await waitFor(() => {
- expect(screen.getByText('AWS Connector 1')).toBeInTheDocument();
- expect(screen.getByText('AWS Connector 2')).toBeInTheDocument();
- });
- });
-
- it('calls setCredentials with correct AWS values when connector is selected', async () => {
- renderWithIntl();
+ return render(
+
+
+
+
+
+ );
+ };
- const select = screen.getByTestId(AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
- await userEvent.click(select);
+ it('should render cloud connector selector', () => {
+ renderSelector();
- await waitFor(() => {
- expect(screen.getByText('AWS Connector 1')).toBeInTheDocument();
- });
-
- await userEvent.click(screen.getByText('AWS Connector 1'));
-
- expect(mockSetCredentials).toHaveBeenCalledWith({
- roleArn: 'arn:aws:iam::123456789012:role/Role1',
- externalId: 'external-id-123',
- cloudConnectorId: 'aws-connector-1',
- });
- });
+ expect(screen.getByText('Cloud Connector Name')).toBeInTheDocument();
+ });
- it('displays selected AWS connector', async () => {
- const propsWithSelection = {
- ...awsProps,
- cloudConnectorId: 'aws-connector-1',
- };
+ it('should display connectors in dropdown', async () => {
+ const user = userEvent.setup();
+ renderSelector();
- renderWithIntl();
+ const selector = screen.getByTestId(AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
+ await user.click(selector);
- await waitFor(() => {
- expect(screen.getByText('AWS Connector 1')).toBeInTheDocument();
- });
+ await waitFor(() => {
+ expect(screen.getByText('AWS Connector 1')).toBeInTheDocument();
+ expect(screen.getByText('AWS Connector 2')).toBeInTheDocument();
});
});
- describe('Azure Provider', () => {
- const azureProps = {
- provider: 'azure' as CloudProvider,
- cloudConnectorId: undefined,
- credentials: {
- tenantId: undefined,
- clientId: undefined,
- azure_credentials_cloud_connector_id: undefined,
- cloudConnectorId: undefined,
- } as AzureCloudConnectorCredentials,
- setCredentials: mockSetCredentials,
- };
-
- beforeEach(() => {
- mockUseGetCloudConnectors.mockReturnValue({
- data: mockAzureCloudConnectors,
- isLoading: false,
- });
+ it('should display edit icon when connector is selected', () => {
+ renderSelector({
+ cloudConnectorId: 'connector-1',
});
- it('displays Azure cloud connectors as options', async () => {
- renderWithIntl();
-
- const select = screen.getByTestId(AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
- await userEvent.click(select);
+ expect(
+ screen.getByTestId(getCloudConnectorEditIconTestSubj('connector-1'))
+ ).toBeInTheDocument();
+ });
- await waitFor(() => {
- expect(screen.getByText('Azure Connector 1')).toBeInTheDocument();
- });
+ it('should open flyout when clicking edit icon on selected connector', async () => {
+ const user = userEvent.setup();
+ renderSelector({
+ cloudConnectorId: 'connector-1',
});
- it('calls setCredentials with correct Azure values when connector is selected', async () => {
- renderWithIntl();
+ const editIcon = screen.getByTestId(getCloudConnectorEditIconTestSubj('connector-1'));
+ await user.click(editIcon);
- const select = screen.getByTestId(AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
- await userEvent.click(select);
+ await waitFor(() => {
+ expect(screen.getByTestId('cloudConnectorPoliciesFlyout')).toBeInTheDocument();
+ });
+ });
- await waitFor(() => {
- expect(screen.getByText('Azure Connector 1')).toBeInTheDocument();
- });
+ it('should call setCredentials when selecting a connector', async () => {
+ const user = userEvent.setup();
+ renderSelector();
- await userEvent.click(screen.getByText('Azure Connector 1'));
+ const selector = screen.getByTestId(AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
+ await user.click(selector);
- expect(mockSetCredentials).toHaveBeenCalledWith({
- tenantId: 'tenant-123',
- clientId: 'client-456',
- azure_credentials_cloud_connector_id: 'azure-cc-789',
- cloudConnectorId: 'azure-connector-1',
- });
+ await waitFor(() => {
+ expect(screen.getByText('AWS Connector 1')).toBeInTheDocument();
});
- it('displays selected Azure connector', async () => {
- const propsWithSelection = {
- ...azureProps,
- cloudConnectorId: 'azure-connector-1',
- };
-
- renderWithIntl();
+ await user.click(screen.getByText('AWS Connector 1'));
- await waitFor(() => {
- expect(screen.getByText('Azure Connector 1')).toBeInTheDocument();
- });
+ expect(mockSetCredentials).toHaveBeenCalledWith({
+ roleArn: 'arn:aws:iam::123456789012:role/Role1',
+ externalId: 'external-id-1',
+ cloudConnectorId: 'connector-1',
});
});
- describe('Hook Integration', () => {
- it('calls useGetCloudConnectors with correct AWS provider', () => {
- mockUseGetCloudConnectors.mockReturnValue({
- data: mockAwsCloudConnectors,
- isLoading: false,
- });
-
- const props = {
- provider: 'aws' as CloudProvider,
- cloudConnectorId: undefined,
- credentials: {
- roleArn: undefined,
- externalId: undefined,
- cloudConnectorId: undefined,
- } as AwsCloudConnectorCredentials,
- setCredentials: mockSetCredentials,
- };
-
- renderWithIntl();
-
- expect(mockUseGetCloudConnectors).toHaveBeenCalledWith('aws');
+ it('should display selected connector', () => {
+ renderSelector({
+ cloudConnectorId: 'connector-1',
});
- it('calls useGetCloudConnectors with correct Azure provider', () => {
- mockUseGetCloudConnectors.mockReturnValue({
- data: mockAzureCloudConnectors,
- isLoading: false,
- });
-
- const props = {
- provider: 'azure' as CloudProvider,
- cloudConnectorId: undefined,
- credentials: {
- tenantId: undefined,
- clientId: undefined,
- azure_credentials_cloud_connector_id: undefined,
- cloudConnectorId: undefined,
- } as AzureCloudConnectorCredentials,
- setCredentials: mockSetCredentials,
- };
-
- renderWithIntl();
-
- expect(mockUseGetCloudConnectors).toHaveBeenCalledWith('azure');
- });
+ expect(screen.getByText('AWS Connector 1')).toBeInTheDocument();
+ });
- it('renders with correct test subject for AWS provider', () => {
- mockUseGetCloudConnectors.mockReturnValue({
- data: mockAwsCloudConnectors,
- isLoading: false,
- });
-
- const props = {
- provider: 'aws' as CloudProvider,
- cloudConnectorId: undefined,
- credentials: {
- roleArn: undefined,
- externalId: undefined,
- cloudConnectorId: undefined,
- } as AwsCloudConnectorCredentials,
- setCredentials: mockSetCredentials,
- };
-
- renderWithIntl();
-
- const select = screen.getByTestId(AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
- expect(select).toBeInTheDocument();
+ it('should not call setCredentials when clicking edit icon', async () => {
+ const user = userEvent.setup();
+ renderSelector({
+ cloudConnectorId: 'connector-1',
});
- it('renders with correct test subject for Azure provider', () => {
- mockUseGetCloudConnectors.mockReturnValue({
- data: mockAzureCloudConnectors,
- isLoading: false,
- });
-
- const props = {
- provider: 'azure' as CloudProvider,
- cloudConnectorId: undefined,
- credentials: {
- tenantId: undefined,
- clientId: undefined,
- azure_credentials_cloud_connector_id: undefined,
- cloudConnectorId: undefined,
- } as AzureCloudConnectorCredentials,
- setCredentials: mockSetCredentials,
- };
-
- renderWithIntl();
-
- const select = screen.getByTestId(AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ);
- expect(select).toBeInTheDocument();
- });
+ const editIcon = screen.getByTestId(getCloudConnectorEditIconTestSubj('connector-1'));
+ await user.click(editIcon);
+
+ // Edit icon click should not trigger connector selection
+ expect(mockSetCredentials).not.toHaveBeenCalled();
});
});
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.tsx
index 2fa2eb45cbf23..40da2ea66aca5 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.tsx
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/form/cloud_connector_selector.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, useState } from 'react';
import {
EuiSuperSelect,
EuiFormRow,
@@ -13,6 +13,8 @@ import {
EuiFlexItem,
EuiText,
EuiTextTruncate,
+ EuiButtonIcon,
+ EuiToolTip,
} from '@elastic/eui';
import type { EuiSuperSelectOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -20,10 +22,12 @@ import { FormattedMessage } from '@kbn/i18n-react';
import {
AWS_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ,
AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ,
+ getCloudConnectorEditIconTestSubj,
} from '@kbn/cloud-security-posture-common';
import type { CloudConnectorCredentials, CloudProviders } from '../types';
import { useGetCloudConnectors } from '../hooks/use_get_cloud_connectors';
import { isAwsCloudConnectorVars, isAzureCloudConnectorVars } from '../utils';
+import { CloudConnectorPoliciesFlyout } from '../cloud_connector_policies_flyout';
interface CloudConnectorSelectorProps {
provider: CloudProviders;
@@ -39,6 +43,8 @@ export const CloudConnectorSelector = ({
setCredentials,
}: CloudConnectorSelectorProps) => {
const { data: cloudConnectors = [] } = useGetCloudConnectors(provider);
+ const [flyoutConnectorId, setFlyoutConnectorId] = useState(null);
+ const [selectKey, setSelectKey] = useState(0);
const label = (
);
+ const handleOpenFlyout = useCallback((e: React.MouseEvent, connectorId: string) => {
+ e.stopPropagation();
+ e.preventDefault();
+ setFlyoutConnectorId(connectorId);
+ // Force dropdown to close by re-rendering the select
+ setSelectKey((prev) => prev + 1);
+ }, []);
+
+ const handleCloseFlyout = useCallback(() => {
+ setFlyoutConnectorId(null);
+ }, []);
+
+ // Find the connector for the flyout
+ const flyoutConnector = useMemo(() => {
+ return cloudConnectors.find((c) => c.id === flyoutConnectorId);
+ }, [cloudConnectors, flyoutConnectorId]);
+
// Create super select options with custom display
const selectOptions: Array> = useMemo(() => {
return cloudConnectors.map((connector) => {
@@ -60,26 +83,63 @@ export const CloudConnectorSelector = ({
return {
value: connector.id,
- inputDisplay: connector.name,
+ inputDisplay: (
+
+
+
+
+
+
+ ) =>
+ handleOpenFlyout(e, connector.id)
+ }
+ data-test-subj={getCloudConnectorEditIconTestSubj(connector.id)}
+ />
+
+
+
+ ),
dropdownDisplay: (
-
+
-
-
-
+
+
+
+
+
+
+ {identifier && (
+
+
+
+
+
+ )}
+
- {identifier && (
-
-
-
-
-
- )}
),
};
});
- }, [cloudConnectors, provider]);
+ }, [cloudConnectors, provider, handleOpenFlyout]);
// Find currently selected value
const selectedValue = useMemo(() => {
@@ -124,21 +184,34 @@ export const CloudConnectorSelector = ({
: AZURE_CLOUD_CONNECTOR_SUPER_SELECT_TEST_SUBJ;
return (
-
-
-
+ <>
+
+
+
+
+ {flyoutConnector && (
+
+ )}
+ >
);
};
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.test.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.test.ts
index f0d8a570551f8..53a0784614bff 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.test.ts
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_setup.test.ts
@@ -18,6 +18,11 @@ jest.mock('../utils', () => ({
updateInputVarsWithCredentials: jest.fn(),
updatePolicyInputs: jest.fn(),
isAzureCloudConnectorVars: jest.fn(),
+ isCloudConnectorNameValid: jest.fn((name: string | undefined) => {
+ if (!name) return false;
+ const trimmedLength = name.trim().length;
+ return trimmedLength > 0 && name.length <= 255;
+ }),
}));
import {
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
index 40fb0b3dc11c0..f7e124a80302b 100644
--- 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
@@ -17,6 +17,7 @@ import {
isAzureCloudConnectorVars,
updateInputVarsWithCredentials,
updatePolicyInputs,
+ isCloudConnectorNameValid,
} from '../utils';
import { AWS_CLOUD_CONNECTOR_FIELD_NAMES, AZURE_CLOUD_CONNECTOR_FIELD_NAMES } from '../constants';
@@ -107,10 +108,8 @@ export const useCloudConnectorSetup = (
const updatedPolicy = { ...newPolicy };
const inputVars = input.streams?.find((i) => i.enabled)?.vars;
- // Determine if name is valid (format only) mimicking the API schema validation
- // Check for trimmed name to prevent whitespace-only strings
- const isNameValid =
- credentials.name && credentials.name.trim().length > 0 && credentials.name.length <= 255;
+ // Use shared validation utility for name validation
+ const isNameValid = isCloudConnectorNameValid(credentials.name);
// Set cloud_connector_name directly on the policy object (not in input vars)
updatedPolicy.cloud_connector_name = credentials.name;
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_usage.test.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_usage.test.ts
new file mode 100644
index 0000000000000..7417ad1dad6d0
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_usage.test.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 { renderHook, waitFor } from '@testing-library/react';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { CLOUD_CONNECTOR_API_ROUTES } from '@kbn/fleet-plugin/public';
+import { QueryClient, QueryClientProvider } from '@kbn/react-query';
+import React from 'react';
+import { useCloudConnectorUsage } from './use_cloud_connector_usage';
+
+jest.mock('@kbn/kibana-react-plugin/public');
+
+const mockHttp = {
+ get: jest.fn(),
+};
+
+const mockUseKibana = useKibana as jest.MockedFunction;
+
+describe('useCloudConnectorUsage', () => {
+ let queryClient: QueryClient;
+
+ beforeEach(() => {
+ // Suppress console.error for expected error tests
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ mockUseKibana.mockReturnValue({
+ services: {
+ http: mockHttp,
+ },
+ } as unknown as ReturnType);
+
+ mockHttp.get.mockClear();
+ });
+
+ afterEach(() => {
+ queryClient.clear();
+ jest.restoreAllMocks();
+ });
+
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
+ React.createElement(QueryClientProvider, { client: queryClient }, children);
+
+ it('should fetch cloud connector usage', async () => {
+ const mockUsageData = {
+ items: [
+ {
+ id: 'policy-1',
+ name: 'Test Policy 1',
+ package: {
+ name: 'cloud_security_posture',
+ title: 'Cloud Security Posture',
+ version: '1.0.0',
+ },
+ policy_ids: ['agent-policy-1'],
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-02T00:00:00Z',
+ },
+ ],
+ };
+
+ mockHttp.get.mockResolvedValue(mockUsageData);
+
+ const { result } = renderHook(() => useCloudConnectorUsage('connector-id-123'), { wrapper });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(mockHttp.get).toHaveBeenCalledWith(
+ CLOUD_CONNECTOR_API_ROUTES.USAGE_PATTERN.replace('{cloudConnectorId}', 'connector-id-123'),
+ { query: { page: 1, perPage: 10 } }
+ );
+ expect(result.current.data).toEqual(mockUsageData);
+ });
+
+ it('should handle errors', async () => {
+ const mockError = new Error('Failed to fetch usage');
+ mockHttp.get.mockRejectedValue(mockError);
+
+ const { result } = renderHook(() => useCloudConnectorUsage('connector-id-123'), { wrapper });
+
+ await waitFor(() => {
+ expect(result.current.isError).toBe(true);
+ });
+
+ expect(result.current.error).toEqual(mockError);
+ });
+
+ it('should not fetch when cloudConnectorId is empty', () => {
+ const { result } = renderHook(() => useCloudConnectorUsage(''), { wrapper });
+
+ expect(mockHttp.get).not.toHaveBeenCalled();
+ expect(result.current.data).toBeUndefined();
+ });
+
+ it('should throw error when http service is not available', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ http: undefined,
+ },
+ } as unknown as ReturnType);
+
+ const { result } = renderHook(() => useCloudConnectorUsage('connector-id-123'), { wrapper });
+
+ await waitFor(() => {
+ expect(result.current.isError).toBe(true);
+ });
+
+ expect(result.current.error).toEqual(new Error('HTTP service is not available'));
+ });
+});
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_usage.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_usage.ts
new file mode 100644
index 0000000000000..3fc13aa03ad48
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_cloud_connector_usage.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 '@kbn/react-query';
+import { CLOUD_CONNECTOR_API_ROUTES } from '@kbn/fleet-plugin/public';
+import type { HttpStart } from '@kbn/core/public';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+
+export interface CloudConnectorUsageItem {
+ id: string;
+ name: string;
+ package?: {
+ name: string;
+ title: string;
+ version: string;
+ };
+ policy_ids: string[];
+ created_at: string;
+ updated_at: string;
+}
+
+interface CloudConnectorUsageResponse {
+ items: CloudConnectorUsageItem[];
+ total: number;
+ page: number;
+ perPage: number;
+}
+
+const fetchCloudConnectorUsage = async (
+ http: HttpStart,
+ cloudConnectorId: string,
+ page: number,
+ perPage: number
+): Promise => {
+ const path = CLOUD_CONNECTOR_API_ROUTES.USAGE_PATTERN.replace(
+ '{cloudConnectorId}',
+ cloudConnectorId
+ );
+
+ return http.get(path, {
+ query: {
+ page,
+ perPage,
+ },
+ });
+};
+
+export const useCloudConnectorUsage = (
+ cloudConnectorId: string,
+ page: number = 1,
+ perPage: number = 10
+) => {
+ const CLOUD_CONNECTOR_USAGE_QUERY_KEY = 'cloud-connector-usage';
+ const { http } = useKibana().services;
+
+ return useQuery(
+ [CLOUD_CONNECTOR_USAGE_QUERY_KEY, cloudConnectorId, page, perPage],
+ () => {
+ if (!http) {
+ throw new Error('HTTP service is not available');
+ }
+ return fetchCloudConnectorUsage(http, cloudConnectorId, page, perPage);
+ },
+ {
+ enabled: !!cloudConnectorId,
+ keepPreviousData: true, // Keep previous data to avoid flashing when going through pages
+ staleTime: 60000, // Cache for 1 minute
+ refetchOnWindowFocus: false,
+ }
+ );
+};
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_update_cloud_connector.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_update_cloud_connector.ts
new file mode 100644
index 0000000000000..bdbf6f85ee13c
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/hooks/use_update_cloud_connector.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { useMutation, useQueryClient } from '@kbn/react-query';
+import { CLOUD_CONNECTOR_API_ROUTES } from '@kbn/fleet-plugin/public';
+import type { HttpStart } from '@kbn/core/public';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { i18n } from '@kbn/i18n';
+
+interface UpdateCloudConnectorRequest {
+ name?: string;
+}
+
+interface CloudConnector {
+ id: string;
+ name: string;
+ namespace?: string;
+ cloudProvider: string;
+ vars: Record;
+ packagePolicyCount: number;
+ created_at: string;
+ updated_at: string;
+}
+
+interface UpdateCloudConnectorResponse {
+ item: CloudConnector;
+}
+
+const updateCloudConnector = async (
+ http: HttpStart,
+ cloudConnectorId: string,
+ data: UpdateCloudConnectorRequest
+): Promise => {
+ const path = CLOUD_CONNECTOR_API_ROUTES.UPDATE_PATTERN.replace(
+ '{cloudConnectorId}',
+ cloudConnectorId
+ );
+
+ return http
+ .put(path, {
+ body: JSON.stringify(data),
+ })
+ .then((res: UpdateCloudConnectorResponse) => res.item);
+};
+
+export const useUpdateCloudConnector = (
+ cloudConnectorId: string,
+ onSuccess?: (connector: CloudConnector) => void,
+ onError?: (error: Error) => void
+) => {
+ const { http, notifications } = useKibana().services;
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ (data: UpdateCloudConnectorRequest) => {
+ if (!http) {
+ throw new Error('HTTP service is not available');
+ }
+ return updateCloudConnector(http, cloudConnectorId, data);
+ },
+ {
+ onSuccess: (connector) => {
+ // Invalidate relevant queries
+ queryClient.invalidateQueries(['get-cloud-connectors']);
+ queryClient.invalidateQueries(['cloud-connector-usage', cloudConnectorId]);
+
+ notifications?.toasts.addSuccess({
+ title: i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnector.updateSuccess',
+ {
+ defaultMessage: 'Cloud connector updated successfully',
+ }
+ ),
+ });
+
+ if (onSuccess) {
+ onSuccess(connector);
+ }
+ },
+ onError: (error: Error) => {
+ notifications?.toasts.addError(error, {
+ title: i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnector.updateError',
+ {
+ defaultMessage: 'Failed to update cloud connector',
+ }
+ ),
+ });
+
+ if (onError) {
+ onError(error);
+ }
+ },
+ }
+ );
+};
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.test.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.test.ts
index 45d9ac7fff052..278c0c7f396a0 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.test.ts
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/components/fleet_extensions/cloud_connector/utils.test.ts
@@ -15,6 +15,9 @@ import {
getCloudConnectorRemoteRoleTemplate,
getKibanaComponentId,
getDeploymentIdFromUrl,
+ getCloudConnectorNameError,
+ isCloudConnectorNameValid,
+ CLOUD_CONNECTOR_NAME_MAX_LENGTH,
} from './utils';
import { getMockPolicyAWS, getMockPackageInfoAWS } from './test/mock';
import type {
@@ -1405,3 +1408,96 @@ describe('getDeploymentIdFromUrl', () => {
expect(result).toBe('deployment-abc-123-xyz');
});
});
+
+describe('Cloud Connector Name Validation', () => {
+ describe('CLOUD_CONNECTOR_NAME_MAX_LENGTH', () => {
+ it('should be 255', () => {
+ expect(CLOUD_CONNECTOR_NAME_MAX_LENGTH).toBe(255);
+ });
+ });
+
+ describe('isCloudConnectorNameValid', () => {
+ it('should return false for undefined name', () => {
+ expect(isCloudConnectorNameValid(undefined)).toBe(false);
+ });
+
+ it('should return false for empty string', () => {
+ expect(isCloudConnectorNameValid('')).toBe(false);
+ });
+
+ it('should return false for whitespace-only string', () => {
+ expect(isCloudConnectorNameValid(' ')).toBe(false);
+ expect(isCloudConnectorNameValid('\t\n')).toBe(false);
+ });
+
+ it('should return true for valid name', () => {
+ expect(isCloudConnectorNameValid('my-connector')).toBe(true);
+ expect(isCloudConnectorNameValid('Test Connector 123')).toBe(true);
+ });
+
+ it('should return true for name with exactly 255 characters', () => {
+ const exactly255Chars = 'a'.repeat(255);
+ expect(isCloudConnectorNameValid(exactly255Chars)).toBe(true);
+ });
+
+ it('should return false for name exceeding 255 characters', () => {
+ const chars256 = 'a'.repeat(256);
+ expect(isCloudConnectorNameValid(chars256)).toBe(false);
+ });
+
+ it('should handle boundary cases correctly', () => {
+ expect(isCloudConnectorNameValid('a'.repeat(254))).toBe(true);
+ expect(isCloudConnectorNameValid('a'.repeat(255))).toBe(true);
+ expect(isCloudConnectorNameValid('a'.repeat(256))).toBe(false);
+ expect(isCloudConnectorNameValid('a'.repeat(257))).toBe(false);
+ });
+
+ it('should handle special characters', () => {
+ expect(isCloudConnectorNameValid('my-connector_123!@#')).toBe(true);
+ });
+
+ it('should handle unicode characters', () => {
+ expect(isCloudConnectorNameValid('connector-中文-名称')).toBe(true);
+ });
+ });
+
+ describe('getCloudConnectorNameError', () => {
+ it('should return required error for undefined name', () => {
+ expect(getCloudConnectorNameError(undefined)).toBe('Cloud Connector Name is required');
+ });
+
+ it('should return required error for empty string', () => {
+ expect(getCloudConnectorNameError('')).toBe('Cloud Connector Name is required');
+ });
+
+ it('should return required error for whitespace-only string', () => {
+ expect(getCloudConnectorNameError(' ')).toBe('Cloud Connector Name is required');
+ expect(getCloudConnectorNameError('\t\n')).toBe('Cloud Connector Name is required');
+ });
+
+ it('should return undefined for valid name', () => {
+ expect(getCloudConnectorNameError('my-connector')).toBeUndefined();
+ expect(getCloudConnectorNameError('Test Connector 123')).toBeUndefined();
+ });
+
+ it('should return undefined for name with exactly 255 characters', () => {
+ const exactly255Chars = 'a'.repeat(255);
+ expect(getCloudConnectorNameError(exactly255Chars)).toBeUndefined();
+ });
+
+ it('should return length error for name exceeding 255 characters', () => {
+ const chars256 = 'a'.repeat(256);
+ expect(getCloudConnectorNameError(chars256)).toBe(
+ 'Cloud Connector Name must be 255 characters or less'
+ );
+ });
+
+ it('should handle boundary cases correctly', () => {
+ expect(getCloudConnectorNameError('a'.repeat(254))).toBeUndefined();
+ expect(getCloudConnectorNameError('a'.repeat(255))).toBeUndefined();
+ expect(getCloudConnectorNameError('a'.repeat(256))).toBe(
+ 'Cloud Connector Name must be 255 characters or less'
+ );
+ });
+ });
+});
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
index db598c7f723a4..77a7b94a8b566 100644
--- 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
@@ -13,6 +13,7 @@ import type {
PackagePolicyConfigRecord,
} from '@kbn/fleet-plugin/common';
import semver from 'semver';
+import { i18n } from '@kbn/i18n';
import type {
AwsCloudConnectorVars,
@@ -51,6 +52,46 @@ export type AzureCloudConnectorFieldNames =
export type AwsCloudConnectorFieldNames =
(typeof AWS_CLOUD_CONNECTOR_FIELD_NAMES)[keyof typeof AWS_CLOUD_CONNECTOR_FIELD_NAMES];
+// Cloud connector name validation constants
+export const CLOUD_CONNECTOR_NAME_MAX_LENGTH = 255;
+
+/**
+ * Validates a cloud connector name
+ * @param name - The name to validate
+ * @returns true if the name is valid, false otherwise
+ */
+export const isCloudConnectorNameValid = (name: string | undefined): boolean => {
+ if (!name) return false;
+ const trimmedLength = name.trim().length;
+ return trimmedLength > 0 && name.length <= CLOUD_CONNECTOR_NAME_MAX_LENGTH;
+};
+
+/**
+ * Gets the validation error message for a cloud connector name
+ * @param name - The name to validate
+ * @returns Error message string or undefined if valid
+ */
+export const getCloudConnectorNameError = (name: string | undefined): string | undefined => {
+ if (!name || name.trim().length === 0) {
+ return i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorNameValidation.requiredError',
+ {
+ defaultMessage: 'Cloud Connector Name is required',
+ }
+ );
+ }
+ if (name.length > CLOUD_CONNECTOR_NAME_MAX_LENGTH) {
+ return i18n.translate(
+ 'securitySolutionPackages.cloudSecurityPosture.cloudConnectorNameValidation.tooLongError',
+ {
+ defaultMessage: 'Cloud Connector Name must be {maxLength} characters or less',
+ values: { maxLength: CLOUD_CONNECTOR_NAME_MAX_LENGTH },
+ }
+ );
+ }
+ return undefined;
+};
+
export const isAwsCloudConnectorVars = (
vars: CloudConnectorVars,
provider: string