import { z } from "zod"; import type { TaskState } from "@/types/tasks"; export const INTEGRATION_TYPE = { AMAZON_S3: "amazon_s3", AWS_SECURITY_HUB: "aws_security_hub", JIRA: "jira", SLACK: "slack", } as const; export type IntegrationType = (typeof INTEGRATION_TYPE)[keyof typeof INTEGRATION_TYPE]; export const INTEGRATION_CONNECTION_TASK_KIND = "integration-connection-test"; export interface IntegrationConnectionTaskResource { id: string; type: "tasks"; } export interface IntegrationConnectionTaskDocument { data: IntegrationConnectionTaskResource; } export interface IntegrationConnectionTaskResult { connected?: boolean; error?: string | null; /** The failing channel id, or null when the failure names no channel. */ channel?: string | null; } export interface IntegrationConnectionTestResponse { success: boolean; message?: string; taskId?: string; data?: IntegrationConnectionTaskDocument; error?: string; /** The failing channel id, or null when the failure names no channel. */ failedChannelId?: string | null; } export const JIRA_DISPATCH_MODE = { INDIVIDUAL: "individual", GROUPED: "grouped", } as const; export type JiraDispatchMode = (typeof JIRA_DISPATCH_MODE)[keyof typeof JIRA_DISPATCH_MODE]; export const JIRA_DISPATCH_TARGET = { CHECK_ID: "check_id", FINDING_ID: "finding_id", } as const; export type JiraDispatchTarget = (typeof JIRA_DISPATCH_TARGET)[keyof typeof JIRA_DISPATCH_TARGET]; export const JIRA_TARGET_SELECTION_KIND = { SINGLE: "single", TARGET_LIST: "target-list", BATCHES: "batches", } as const; export type JiraTargetSelectionKind = (typeof JIRA_TARGET_SELECTION_KIND)[keyof typeof JIRA_TARGET_SELECTION_KIND]; export type NonEmptyStringArray = [string, ...string[]]; export interface JiraDispatchTargetBatch { targetIds: NonEmptyStringArray; targetType: JiraDispatchTarget; dispatchMode?: JiraDispatchMode; } export interface JiraSingleTargetSelection { kind: typeof JIRA_TARGET_SELECTION_KIND.SINGLE; targetId: string; targetType: JiraDispatchTarget; } export interface JiraTargetListSelection { kind: typeof JIRA_TARGET_SELECTION_KIND.TARGET_LIST; targetIds: NonEmptyStringArray; targetType: JiraDispatchTarget; } export interface JiraBatchSelection { kind: typeof JIRA_TARGET_SELECTION_KIND.BATCHES; batches: [JiraDispatchTargetBatch, ...JiraDispatchTargetBatch[]]; } export type JiraSelection = | JiraSingleTargetSelection | JiraTargetListSelection | JiraBatchSelection; export const JIRA_DISPATCH_TASK_KIND = "jira-dispatch"; export interface IntegrationProps { type: "integrations"; id: string; attributes: { inserted_at: string; updated_at: string; enabled: boolean; // `null` until a connection check has run: never verified, neither working // nor broken. A Slack install starts here, and returns here on a channel // change. connected: boolean | null; connection_last_checked_at: string | null; integration_type: IntegrationType; configuration: { bucket_name?: string; output_directory?: string; credentials?: { aws_access_key_id?: string; aws_secret_access_key?: string; aws_session_token?: string; role_arn?: string; external_id?: string; role_session_name?: string; session_duration?: number; }; // Jira specific configuration domain?: string; projects?: { [key: string]: string }; issue_types?: { [key: string]: string[] }; // Slack specific configuration, server-owned. The keys are optional here // because the shape is shared with every integration: read with `?? []`. team_id?: string; team_name?: string; bot_user_id?: string; channels?: SlackAuthorizedChannel[]; verification?: SlackVerification; [key: string]: unknown; }; url?: string; }; relationships?: { providers?: { data: { type: "providers"; id: string }[] } }; links: { self: string }; } /** * A channel Prowler can post to: every active public channel, plus the private * ones `@Prowler Cloud` was invited to. `is_private` keeps the API's own naming. */ export interface SlackChannelOption { id: string; name: string; is_private: boolean; } /** * `confirmation_sent_at` is null until a connection check posts one, and null * again after a same-workspace reinstall (contract, OAuth and reads). */ export interface SlackAuthorizedChannel extends SlackChannelOption { confirmation_sent_at: string | null; } /** * The contract's `verification` block in full, though nothing reads it yet * (contract, OAuth and reads). */ export interface SlackVerification { task_id: string | null; started_at: string | null; finished_at: string | null; } // Jira dispatch types export interface JiraDispatchRequest { data: { type: "integrations-jira-dispatches"; attributes: { project_key: string; issue_type: string; dispatch_mode?: JiraDispatchMode; }; }; } export interface JiraDispatchResponse { data: { type: "tasks"; id: string; attributes: { inserted_at: string; completed_at: string | null; name: string; state: TaskState; result: JiraDispatchTaskResult | null; task_args: Record | null; metadata: Record | null; }; }; } export interface JiraDispatchTaskResult { success?: boolean; error?: string; message?: string; successful_count?: number; created_count?: number; updated_count?: number; failed_count?: number; created_issues?: unknown[]; updated_issues?: unknown[]; failed_groups?: unknown[]; failed_batches?: unknown[]; failed_finding_ids?: string[]; issue_url?: string; issue_key?: string; } // Shared AWS credential fields schema const awsCredentialFields = { credentials_type: z.enum(["aws-sdk-default", "access-secret-key"]), aws_access_key_id: z.string().optional(), aws_secret_access_key: z.string().optional(), aws_session_token: z.string().optional(), role_arn: z.string().optional(), external_id: z.string().optional(), role_session_name: z.string().optional(), session_duration: z.string().optional(), show_role_section: z.boolean().optional(), }; // Shared validation helper for AWS credentials (create mode) type AwsCredentialsData = { credentials_type?: "aws-sdk-default" | "access-secret-key"; aws_access_key_id?: string; aws_secret_access_key?: string; aws_session_token?: string; role_arn?: string; external_id?: string; role_session_name?: string; session_duration?: string; show_role_section?: boolean; }; const validateAwsCredentialsCreate = ( data: AwsCredentialsData, ctx: z.RefinementCtx, requireCredentials: boolean = true, ) => { if (data.credentials_type === "access-secret-key" && requireCredentials) { if (!data.aws_access_key_id) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "AWS Access Key ID is required when using access and secret key", path: ["aws_access_key_id"], }); } if (!data.aws_secret_access_key) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "AWS Secret Access Key is required when using access and secret key", path: ["aws_secret_access_key"], }); } } }; // Shared validation helper for AWS credentials (edit mode) const validateAwsCredentialsEdit = ( data: AwsCredentialsData, ctx: z.RefinementCtx, ) => { if (data.credentials_type === "access-secret-key") { const hasAccessKey = !!data.aws_access_key_id; const hasSecretKey = !!data.aws_secret_access_key; if (hasAccessKey && !hasSecretKey) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "AWS Secret Access Key is required when providing Access Key ID", path: ["aws_secret_access_key"], }); } if (hasSecretKey && !hasAccessKey) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "AWS Access Key ID is required when providing Secret Access Key", path: ["aws_access_key_id"], }); } } }; // Shared validation helper for IAM Role fields const validateIamRole = ( data: AwsCredentialsData, ctx: z.RefinementCtx, checkShowSection: boolean = true, ) => { const shouldValidate = checkShowSection ? data.show_role_section === true : true; if (shouldValidate && data.role_arn) { if (data.role_arn.trim() === "") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role ARN is required", path: ["role_arn"], }); } else if (!data.external_id || data.external_id.trim() === "") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "External ID is required when using Role ARN", path: ["external_id"], }); } } if (checkShowSection && data.show_role_section === true) { if (!data.role_arn || data.role_arn.trim() === "") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role ARN is required", path: ["role_arn"], }); } if (!data.external_id || data.external_id.trim() === "") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "External ID is required", path: ["external_id"], }); } } }; // S3 Integration Schemas const baseS3IntegrationSchema = z.object({ integration_type: z.literal("amazon_s3"), bucket_name: z.string().min(1, "Bucket name is required"), output_directory: z.string().min(1, "Output directory is required"), // UI-only field used to prefill the S3IntegrationBucketAccountId parameter of // the CloudFormation quick-create link. Not sent to the backend. bucket_account_id: z .string() .optional() .refine((value) => !value || /^\d{12}$/.test(value), { error: "Must be a valid 12-digit AWS Account ID", }), providers: z.array(z.string()).optional(), enabled: z.boolean().optional(), ...awsCredentialFields, }); export const s3IntegrationFormSchema = baseS3IntegrationSchema .extend({ enabled: z.boolean().default(true), credentials_type: z .enum(["aws-sdk-default", "access-secret-key"]) .default("aws-sdk-default"), }) .superRefine((data, ctx) => { validateAwsCredentialsCreate(data, ctx); validateIamRole(data, ctx); }); export const editS3IntegrationFormSchema = baseS3IntegrationSchema .extend({ bucket_name: z.string().min(1, "Bucket name is required").optional(), output_directory: z .string() .min(1, "Output directory is required") .optional(), providers: z.array(z.string()).optional(), credentials_type: z .enum(["aws-sdk-default", "access-secret-key"]) .optional(), }) .superRefine((data, ctx) => { validateAwsCredentialsEdit(data, ctx); validateIamRole(data, ctx); }); // Security Hub Integration Schemas const baseSecurityHubIntegrationSchema = z.object({ integration_type: z.literal("aws_security_hub"), provider_id: z.string().min(1, "AWS Provider is required"), send_only_fails: z.boolean().optional(), archive_previous_findings: z.boolean().optional(), use_custom_credentials: z.boolean().optional(), enabled: z.boolean().optional(), ...awsCredentialFields, }); export const securityHubIntegrationFormSchema = baseSecurityHubIntegrationSchema .extend({ enabled: z.boolean().default(true), send_only_fails: z.boolean().default(true), archive_previous_findings: z.boolean().default(false), use_custom_credentials: z.boolean().default(false), credentials_type: z .enum(["aws-sdk-default", "access-secret-key"]) .default("aws-sdk-default"), }) .superRefine((data, ctx) => { if (data.use_custom_credentials) { validateAwsCredentialsCreate(data, ctx); validateIamRole(data, ctx); } // Always validate role if role_arn is provided if (!data.use_custom_credentials && data.role_arn) { validateIamRole(data, ctx, false); } }); export const editSecurityHubIntegrationFormSchema = baseSecurityHubIntegrationSchema .extend({ provider_id: z.string().optional(), send_only_fails: z.boolean().optional(), archive_previous_findings: z.boolean().optional(), use_custom_credentials: z.boolean().optional(), credentials_type: z .enum(["aws-sdk-default", "access-secret-key"]) .optional(), }) .superRefine((data, ctx) => { if (data.use_custom_credentials !== false) { validateAwsCredentialsEdit(data, ctx); } // Always validate role if role_arn is provided validateIamRole(data, ctx, false); }); // Jira Integration Schemas export const jiraIntegrationFormSchema = z.object({ integration_type: z.literal("jira"), domain: z.string().min(1, "Domain is required"), user_mail: z.email({ error: "Invalid email format" }), api_token: z.string().min(1, "API token is required"), enabled: z.boolean().default(true), }); export const editJiraIntegrationFormSchema = z.object({ integration_type: z.literal("jira"), domain: z.string().min(1, "Domain is required").optional(), user_mail: z.email({ error: "Invalid email format" }).optional(), api_token: z.string().min(1, "API token is required").optional(), }); export type CreateValues = z.infer; export type EditValues = z.infer; export type FormValues = CreateValues | EditValues; export interface JiraCredentialsPayload { domain?: string; user_mail?: string; api_token?: string; }