diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts index 8aa7887e4d..21fd4119b7 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts @@ -1,16 +1,16 @@ import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints"; import { - BitBucketDataSourceSchema, - CreateBitBucketDataSourceSchema, - UpdateBitBucketDataSourceSchema + BitbucketDataSourceSchema, + CreateBitbucketDataSourceSchema, + UpdateBitbucketDataSourceSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -export const registerBitBucketSecretScanningRouter = async (server: FastifyZodProvider) => +export const registerBitbucketSecretScanningRouter = async (server: FastifyZodProvider) => registerSecretScanningEndpoints({ - type: SecretScanningDataSource.BitBucket, + type: SecretScanningDataSource.Bitbucket, server, - responseSchema: BitBucketDataSourceSchema, - createSchema: CreateBitBucketDataSourceSchema, - updateSchema: UpdateBitBucketDataSourceSchema + responseSchema: BitbucketDataSourceSchema, + createSchema: CreateBitbucketDataSourceSchema, + updateSchema: UpdateBitbucketDataSourceSchema }); diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts index ff7c304f6d..2258f9c823 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts @@ -1,6 +1,6 @@ import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -import { registerBitBucketSecretScanningRouter } from "./bitbucket-secret-scanning-router"; +import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router"; import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router"; export * from "./secret-scanning-v2-router"; @@ -10,5 +10,5 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record< (server: FastifyZodProvider) => Promise > = { [SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter, - [SecretScanningDataSource.BitBucket]: registerBitBucketSecretScanningRouter + [SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter }; diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts index ed7b66a19a..0a672437dd 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { SecretScanningConfigsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { BitBucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; +import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github"; import { SecretScanningFindingStatus, @@ -24,7 +24,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [ GitHubDataSourceListItemSchema, - BitBucketDataSourceListItemSchema + BitbucketDataSourceListItemSchema ]); export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index c2db3e6e78..dabcf20174 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -18,33 +18,33 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, - pitRecovery: false, - ipAllowlisting: false, - rbac: false, - githubOrgSync: false, + pitRecovery: true, + ipAllowlisting: true, + rbac: true, + githubOrgSync: true, customRateLimits: false, - customAlerts: false, - secretAccessInsights: false, - auditLogs: false, - auditLogsRetentionDays: 0, - auditLogStreams: false, + customAlerts: true, + secretAccessInsights: true, + auditLogs: true, + auditLogsRetentionDays: 3, + auditLogStreams: true, auditLogStreamLimit: 3, - samlSSO: false, - hsm: false, - oidcSSO: false, - scim: false, - ldap: false, - groups: false, + samlSSO: true, + hsm: true, + oidcSSO: true, + scim: true, + ldap: true, + groups: true, status: null, trial_end: null, has_used_trial: true, - secretApproval: false, - secretRotation: false, - caCrl: false, - instanceUserManagement: false, - externalKms: false, + secretApproval: true, + secretRotation: true, + caCrl: true, + instanceUserManagement: true, + externalKms: true, rateLimits: { readLimit: 60, writeLimit: 200, @@ -52,13 +52,13 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ }, pkiEst: false, enforceMfa: false, - projectTemplates: false, - kmip: false, - gateway: false, - sshHostGroups: false, - secretScanning: false, - enterpriseSecretSyncs: false, - enterpriseAppConnections: false + projectTemplates: true, + kmip: true, + gateway: true, + sshHostGroups: true, + enterpriseAppConnections: true, + enterpriseSecretSyncs: true, + secretScanning: true }); export const setupLicenseRequestWithStore = ( diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts index 0b93359154..80d22c64b2 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts @@ -3,7 +3,7 @@ import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scann import { AppConnection } from "@app/services/app-connection/app-connection-enums"; export const BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = { - name: "BitBucket", - type: SecretScanningDataSource.BitBucket, - connection: AppConnection.BitBucket + name: "Bitbucket", + type: SecretScanningDataSource.Bitbucket, + connection: AppConnection.Bitbucket }; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts index 071133be8f..6840548c5a 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts @@ -3,7 +3,6 @@ import { join } from "path"; import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; import { - SecretScanningDataSource, SecretScanningFindingSeverity, SecretScanningResource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; @@ -20,54 +19,106 @@ import { TSecretScanningFactoryListRawResources, TSecretScanningFactoryPostInitialization } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; import { GitHubRepositoryRegex } from "@app/lib/regex"; import { - getBitBucketUser, - listBitBucketRepositories, - TBitBucketConnection + getBitbucketUser, + listBitbucketRepositories, + TBitbucketConnection } from "@app/services/app-connection/bitbucket"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; -import { TBitBucketDataSourceWithConnection, TQueueBitBucketResourceDiffScan } from "./bitbucket-secret-scanning-types"; +import { TBitbucketDataSourceWithConnection, TQueueBitbucketResourceDiffScan } from "./bitbucket-secret-scanning-types"; -export const BitBucketSecretScanningFactory = () => { - const initialize: TSecretScanningFactoryInitialize = async ( - { connection, secretScanningV2DAL }, +export const BitbucketSecretScanningFactory = () => { + const initialize: TSecretScanningFactoryInitialize = async ( + { connection, payload }, callback ) => { - // TODO(andrey): Swap for something proper - const externalId = connection.credentials.email; + const { email, apiToken } = connection.credentials; + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; - const existingDataSource = await secretScanningV2DAL.dataSources.findOne({ - externalId, - type: SecretScanningDataSource.BitBucket - }); - - if (existingDataSource) - throw new BadRequestError({ - message: `A Data Source already exists for this BitBucket Radar Connection in the Project with ID "${existingDataSource.projectId}"` - }); + const { data } = await request.post<{ uuid: string }>( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks`, + { + description: "Infisical webhook for push events", + url: `https://tunnel.util.lol/secret-scanning/webhooks/bitbucket`, // TODO(andrey): Swap to ${cfg.SITE_URL} + active: true, + events: ["repo:push"] + }, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); return callback({ - externalId + credentials: { webhookId: data.uuid } }); }; - const postInitialization: TSecretScanningFactoryPostInitialization = async () => { - // no post-initialization required + const postInitialization: TSecretScanningFactoryPostInitialization = async ({ + dataSourceId, + credentials, + connection, + payload + }) => { + const { email, apiToken } = connection.credentials; + const { webhookId } = credentials; + + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + const cfg = getConfig(); + const newWebhookUrl = `${cfg.SITE_URL}/secret-scanning/webhooks/bitbucket?dataSourceId=${dataSourceId}`; + + await request.put( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks/${webhookId}`, + { + description: "Infisical webhook for push events", + url: newWebhookUrl, + active: true, + events: ["repo:push"] + }, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); }; - const listRawResources: TSecretScanningFactoryListRawResources = async ( + // TODO(andrey): Hook this up + const postDeletion: any = async ({ credentials, connection, payload }) => { + const { email, apiToken } = connection.credentials; + const { webhookId } = credentials; + + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + await request.delete( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks/${webhookId}`, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); + }; + + const listRawResources: TSecretScanningFactoryListRawResources = async ( dataSource ) => { const { connection, - config: { includeRepos } + config: { includeRepos, workspaceSlug } } = dataSource; - const repos = await listBitBucketRepositories(connection); + const repos = await listBitbucketRepositories(connection, workspaceSlug); const filteredRepos: typeof repos = []; if (includeRepos.includes("*")) { @@ -83,7 +134,7 @@ export const BitBucketSecretScanningFactory = () => { })); }; - const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ + const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ dataSource, resourceName, tempFolder @@ -97,10 +148,10 @@ export const BitBucketSecretScanningFactory = () => { const repoPath = join(tempFolder, "repo.git"); if (!GitHubRepositoryRegex.test(resourceName)) { - throw new Error("Invalid BitBucket repository name"); + throw new Error("Invalid Bitbucket repository name"); } - const { username } = await getBitBucketUser({ email, apiToken }); + const { username } = await getBitbucketUser({ email, apiToken }); await cloneRepository({ cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`, @@ -111,18 +162,18 @@ export const BitBucketSecretScanningFactory = () => { }; const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload< - TQueueBitBucketResourceDiffScan["payload"] - > = ({ repository }) => { + TQueueBitbucketResourceDiffScan["payload"] + > = ({ repository, dataSourceId }) => { return { name: repository.full_name, - externalId: repository.id.toString(), + externalId: dataSourceId, type: SecretScanningResource.Repository }; }; const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload< - TBitBucketDataSourceWithConnection, - TQueueBitBucketResourceDiffScan["payload"] + TBitbucketDataSourceWithConnection, + TQueueBitbucketResourceDiffScan["payload"] > = async ({ dataSource, payload, resourceName, configPath }) => { const { connection: { @@ -130,81 +181,86 @@ export const BitBucketSecretScanningFactory = () => { } } = dataSource; - const { commits, repository } = payload; + const { push, repository } = payload; const allFindings: SecretMatch[] = []; const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; - for (const commit of commits) { - // eslint-disable-next-line no-await-in-loop - const { data: diffstat } = await request.get<{ - values: { - status: "added" | "modified" | "removed" | "renamed"; - new?: { path: string }; - old?: { path: string }; - }[]; - }>(`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diffstat/${commit.id}`, { - headers: { - Authorization: authHeader, - Accept: "application/json" - } - }); + for (const change of push.changes) { + for (const commit of change.commits) { + // eslint-disable-next-line no-await-in-loop + const { data: diffstat } = await request.get<{ + values: { + status: "added" | "modified" | "removed" | "renamed"; + new?: { path: string }; + old?: { path: string }; + }[]; + }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${repository.full_name}/diffstat/${commit.hash}`, { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + }); - // eslint-disable-next-line no-continue - if (!diffstat.values) continue; + // eslint-disable-next-line no-continue + if (!diffstat.values) continue; - for (const file of diffstat.values) { - if ((file.status === "added" || file.status === "modified") && file.new?.path) { - const filePath = file.new.path; + for (const file of diffstat.values) { + if ((file.status === "added" || file.status === "modified") && file.new?.path) { + const filePath = file.new.path; - // eslint-disable-next-line no-await-in-loop - const { data: patch } = await request.get( - `https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.id}`, - { - params: { - path: filePath - }, - headers: { - Authorization: authHeader - }, - responseType: "text" - } - ); + // eslint-disable-next-line no-await-in-loop + const { data: patch } = await request.get( + `https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.hash}`, + { + params: { + path: filePath + }, + headers: { + Authorization: authHeader + }, + responseType: "text" + } + ); - // eslint-disable-next-line no-continue - if (!patch) continue; + // eslint-disable-next-line no-continue + if (!patch) continue; - // eslint-disable-next-line - const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); + // eslint-disable-next-line no-await-in-loop + const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); - const adjustedFindings = findings.map((finding) => { - const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine); - const endLine = - finding.StartLine === finding.EndLine - ? startLine - : convertPatchLineToFileLineNumber(patch, finding.EndLine); - const startColumn = finding.StartColumn - 1; // subtract 1 for + - const endColumn = finding.EndColumn - 1; // subtract 1 for + + const adjustedFindings = findings.map((finding) => { + const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine); + const endLine = + finding.StartLine === finding.EndLine + ? startLine + : convertPatchLineToFileLineNumber(patch, finding.EndLine); + const startColumn = finding.StartColumn - 1; // subtract 1 for + + const endColumn = finding.EndColumn - 1; // subtract 1 for + + const authorName = commit.author.user?.display_name || commit.author.raw.split(" <")[0]; + const emailMatch = commit.author.raw.match(/<(.*)>/); + const authorEmail = emailMatch?.[1] ?? ""; - return { - ...finding, - StartLine: startLine, - EndLine: endLine, - StartColumn: startColumn, - EndColumn: endColumn, - File: filePath, - Commit: commit.id, - Author: commit.author.name, - Email: commit.author.email ?? "", - Message: commit.message, - Fingerprint: `${commit.id}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`, - Date: commit.timestamp, - Link: `https://bitbucket.org/${resourceName}/src/${commit.id}/${filePath}#lines-${startLine}` - }; - }); + return { + ...finding, + StartLine: startLine, + EndLine: endLine, + StartColumn: startColumn, + EndColumn: endColumn, + File: filePath, + Commit: commit.hash, + Author: authorName, + Email: authorEmail, + Message: commit.message, + Fingerprint: `${commit.hash}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`, + Date: commit.date, + Link: `https://bitbucket.org/${resourceName}/src/${commit.hash}/${filePath}#lines-${startLine}` + }; + }); - allFindings.push(...adjustedFindings); + allFindings.push(...adjustedFindings); + } } } } diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts index 830b5c5be6..a7a0d7f145 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts @@ -15,7 +15,12 @@ import { SecretScanningDataSources } from "@app/lib/api-docs"; import { GitHubRepositoryRegex } from "@app/lib/regex"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; -export const BitBucketDataSourceConfigSchema = z.object({ +export const BitbucketDataSourceConfigSchema = z.object({ + workspaceSlug: z + .string() + .min(1, "Workspace slug required") + .max(128) + .describe(SecretScanningDataSources.CONFIG.BITBUCKET.workspaceSlug), includeRepos: z .array( z @@ -30,58 +35,62 @@ export const BitBucketDataSourceConfigSchema = z.object({ .describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos) }); -export const BitBucketDataSourceSchema = BaseSecretScanningDataSourceSchema({ - type: SecretScanningDataSource.BitBucket, +export const BitbucketDataSourceSchema = BaseSecretScanningDataSourceSchema({ + type: SecretScanningDataSource.Bitbucket, isConnectionRequired: true }) .extend({ - config: BitBucketDataSourceConfigSchema + config: BitbucketDataSourceConfigSchema }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const CreateBitBucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({ - type: SecretScanningDataSource.BitBucket, +export const CreateBitbucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({ + type: SecretScanningDataSource.Bitbucket, isConnectionRequired: true }) .extend({ - config: BitBucketDataSourceConfigSchema + config: BitbucketDataSourceConfigSchema }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const UpdateBitBucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema( - SecretScanningDataSource.BitBucket +export const UpdateBitbucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema( + SecretScanningDataSource.Bitbucket ) .extend({ - config: BitBucketDataSourceConfigSchema.optional() + config: BitbucketDataSourceConfigSchema.optional() }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const BitBucketDataSourceListItemSchema = z +export const BitbucketDataSourceListItemSchema = z .object({ - name: z.literal("BitBucket"), - connection: z.literal(AppConnection.BitBucket), - type: z.literal(SecretScanningDataSource.BitBucket) + name: z.literal("Bitbucket"), + connection: z.literal(AppConnection.Bitbucket), + type: z.literal(SecretScanningDataSource.Bitbucket) }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const BitBucketFindingSchema = BaseSecretScanningFindingSchema.extend({ +export const BitbucketFindingSchema = BaseSecretScanningFindingSchema.extend({ resourceType: z.literal(SecretScanningResource.Repository), - dataSourceType: z.literal(SecretScanningDataSource.BitBucket), + dataSourceType: z.literal(SecretScanningDataSource.Bitbucket), details: GitRepositoryScanFindingDetailsSchema }); + +export const BitbucketDataSourceCredentialsSchema = z.object({ + webhookId: z.string() +}); diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts index 508aef5290..ad5a594533 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts @@ -1,11 +1,9 @@ -import { PushEvent } from "@octokit/webhooks-types"; - import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue"; import { logger } from "@app/lib/logger"; -import { TBitBucketDataSource } from "./bitbucket-secret-scanning-types"; +import { TBitbucketDataSource, TBitbucketPushEvent } from "./bitbucket-secret-scanning-types"; export const bitBucketSecretScanningService = ( secretScanningV2DAL: TSecretScanningV2DALFactory, @@ -14,18 +12,18 @@ export const bitBucketSecretScanningService = ( const handleInstallationDeletedEvent = async (installationId: number) => { const dataSource = await secretScanningV2DAL.dataSources.findOne({ externalId: String(installationId), - type: SecretScanningDataSource.BitBucket + type: SecretScanningDataSource.Bitbucket }); if (!dataSource) { logger.error( - `secretScanningV2RemoveEvent: BitBucket - Could not find data source [installationId=${installationId}]` + `secretScanningV2RemoveEvent: Bitbucket - Could not find data source [installationId=${installationId}]` ); return; } logger.info( - `secretScanningV2RemoveEvent: BitBucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]` + `secretScanningV2RemoveEvent: Bitbucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]` ); await secretScanningV2DAL.dataSources.updateById(dataSource.id, { @@ -33,24 +31,26 @@ export const bitBucketSecretScanningService = ( }); }; - const handlePushEvent = async (payload: PushEvent) => { - const { commits, repository, installation } = payload; + const handlePushEvent = async (payload: TBitbucketPushEvent & { dataSourceId: string }) => { + const { push, repository } = payload; - if (!commits || !repository || !installation) { + if (!push?.changes?.length || !repository?.workspace?.uuid) { logger.warn( - `secretScanningV2PushEvent: BitBucket - Insufficient data [commits=${commits?.length ?? 0}] [repository=${repository.name}] [installationId=${installation?.id}]` + `secretScanningV2PushEvent: Bitbucket - Insufficient data [changes=${ + push?.changes?.length ?? 0 + }] [repository=${repository?.name}] [workspaceUuid=${repository?.workspace?.uuid}]` ); return; } const dataSource = (await secretScanningV2DAL.dataSources.findOne({ - externalId: String(installation.id), - type: SecretScanningDataSource.BitBucket - })) as TBitBucketDataSource | undefined; + externalId: payload.dataSourceId, + type: SecretScanningDataSource.Bitbucket + })) as TBitbucketDataSource | undefined; if (!dataSource) { logger.error( - `secretScanningV2PushEvent: BitBucket - Could not find data source [installationId=${installation.id}]` + `secretScanningV2PushEvent: Bitbucket - Could not find data source [workspaceUuid=${repository.workspace.uuid}]` ); return; } @@ -62,20 +62,20 @@ export const bitBucketSecretScanningService = ( if (!isAutoScanEnabled) { logger.info( - `secretScanningV2PushEvent: BitBucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [installationId=${installation.id}]` + `secretScanningV2PushEvent: Bitbucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [workspaceUuid=${repository.workspace.uuid}]` ); return; } if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) { await secretScanningV2Queue.queueResourceDiffScan({ - dataSourceType: SecretScanningDataSource.BitBucket, + dataSourceType: SecretScanningDataSource.Bitbucket, payload, dataSourceId: dataSource.id }); } else { logger.info( - `secretScanningV2PushEvent: BitBucket - ignoring due to repository not being present in config [installationId=${installation.id}] [dataSourceId=${dataSource.id}]` + `secretScanningV2PushEvent: Bitbucket - ignoring due to repository not being present in config [workspaceUuid=${repository.workspace.uuid}] [dataSourceId=${dataSource.id}]` ); } }; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts index 5df73cbda4..03e3f81134 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts @@ -1,31 +1,84 @@ -import { PushEvent } from "@octokit/webhooks-types"; import { z } from "zod"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -import { TBitBucketConnection } from "@app/services/app-connection/bitbucket"; +import { TBitbucketConnection } from "@app/services/app-connection/bitbucket"; import { - BitBucketDataSourceListItemSchema, - BitBucketDataSourceSchema, - BitBucketFindingSchema, - CreateBitBucketDataSourceSchema + BitbucketDataSourceCredentialsSchema, + BitbucketDataSourceListItemSchema, + BitbucketDataSourceSchema, + BitbucketFindingSchema, + CreateBitbucketDataSourceSchema } from "./bitbucket-secret-scanning-schemas"; -export type TBitBucketDataSource = z.infer; +export type TBitbucketDataSource = z.infer; -export type TBitBucketDataSourceInput = z.infer; +export type TBitbucketDataSourceInput = z.infer; -export type TBitBucketDataSourceListItem = z.infer; +export type TBitbucketDataSourceListItem = z.infer; -export type TBitBucketFinding = z.infer; +export type TBitbucketDataSourceCredentials = z.infer; -export type TBitBucketDataSourceWithConnection = TBitBucketDataSource & { - connection: TBitBucketConnection; +export type TBitbucketFinding = z.infer; + +export type TBitbucketDataSourceWithConnection = TBitbucketDataSource & { + connection: TBitbucketConnection; }; -export type TQueueBitBucketResourceDiffScan = { - dataSourceType: SecretScanningDataSource.BitBucket; - payload: PushEvent; +export type TBitbucketPushEventRepository = { + full_name: string; + name: string; + workspace: { + slug: string; + uuid: string; + }; + uuid: string; +}; + +export type TBitbucketPushEventCommit = { + hash: string; + message: string; + author: { + raw: string; + user?: { + display_name: string; + uuid: string; + nickname: string; + }; + }; + date: string; +}; + +export type TBitbucketPushEventChange = { + new?: { + name: string; + type: string; + }; + old?: { + name: string; + type: string; + }; + created: boolean; + closed: boolean; + forced: boolean; + commits: TBitbucketPushEventCommit[]; +}; + +export type TBitbucketPushEvent = { + push: { + changes: TBitbucketPushEventChange[]; + }; + repository: TBitbucketPushEventRepository; + actor: { + display_name: string; + uuid: string; + nickname: string; + }; +}; + +export type TQueueBitbucketResourceDiffScan = { + dataSourceType: SecretScanningDataSource.Bitbucket; + payload: TBitbucketPushEvent & { dataSourceId: string }; dataSourceId: string; resourceId: string; scanId: string; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts index 91ce3c8c47..40e5ea7dd5 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts @@ -1,6 +1,6 @@ export enum SecretScanningDataSource { GitHub = "github", - BitBucket = "bitbucket" + Bitbucket = "bitbucket" } export enum SecretScanningScanStatus { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts index a0f7d6c652..76e72ab773 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts @@ -1,4 +1,4 @@ -import { BitBucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory"; +import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory"; import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory"; import { SecretScanningDataSource } from "./secret-scanning-v2-enums"; @@ -17,5 +17,5 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory< export const SECRET_SCANNING_FACTORY_MAP: Record = { [SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation, - [SecretScanningDataSource.BitBucket]: BitBucketSecretScanningFactory as TSecretScanningFactoryImplementation + [SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 8a3729c1c7..9489f46589 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -13,7 +13,7 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record = { [SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION, - [SecretScanningDataSource.BitBucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION + [SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION }; export const listSecretScanningDataSourceOptions = () => { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts index c876a1793c..c84d6056a3 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts @@ -3,15 +3,15 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record = { [SecretScanningDataSource.GitHub]: "GitHub", - [SecretScanningDataSource.BitBucket]: "BitBucket" + [SecretScanningDataSource.Bitbucket]: "Bitbucket" }; export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record = { [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar, - [SecretScanningDataSource.BitBucket]: AppConnection.BitBucket + [SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket }; export const AUTO_SYNC_DESCRIPTION_HELPER: Record = { [SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }, - [SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" } + [SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" } }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts index 3342a8906f..5c11a8ffb7 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts @@ -5,12 +5,13 @@ import { TSecretScanningScans } from "@app/db/schemas"; import { - TBitBucketDataSource, - TBitBucketDataSourceInput, - TBitBucketDataSourceListItem, - TBitBucketDataSourceWithConnection, - TBitBucketFinding, - TQueueBitBucketResourceDiffScan + TBitbucketDataSource, + TBitbucketDataSourceCredentials, + TBitbucketDataSourceInput, + TBitbucketDataSourceListItem, + TBitbucketDataSourceWithConnection, + TBitbucketFinding, + TQueueBitbucketResourceDiffScan } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { TGitHubDataSource, @@ -27,7 +28,7 @@ import { SecretScanningScanStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource; +export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource; export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & { lastScannedAt?: Date | null; @@ -51,13 +52,15 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & { export type TSecretScanningDataSourceWithConnection = | TGitHubDataSourceWithConnection - | TBitBucketDataSourceWithConnection; + | TBitbucketDataSourceWithConnection; -export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitBucketDataSourceInput; +export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitbucketDataSourceInput; -export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitBucketDataSourceListItem; +export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitbucketDataSourceListItem; -export type TSecretScanningFinding = TGitHubFinding | TBitBucketFinding; +export type TDataSourceCredentialsSchema = TBitbucketDataSourceCredentials | undefined; + +export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding; export type TListSecretScanningDataSourcesByProjectId = { projectId: string; @@ -109,7 +112,7 @@ export type TQueueSecretScanningDataSourceFullScan = { scanId: string; }; -export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitBucketResourceDiffScan; +export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan; export type TQueueSecretScanningSendNotification = { dataSource: TSecretScanningDataSources; @@ -149,7 +152,7 @@ export type TSecretScanningDataSourceRaw = NonNullable< export type TSecretScanningFactoryInitialize< T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, - C extends TSecretScanningDataSourceCredentials = undefined + C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema > = ( params: { payload: TCreateSecretScanningDataSourceDTO; @@ -161,7 +164,7 @@ export type TSecretScanningFactoryInitialize< export type TSecretScanningFactoryPostInitialization< T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, - C extends TSecretScanningDataSourceCredentials = undefined + C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema > = (params: { payload: TCreateSecretScanningDataSourceDTO; connection: T; @@ -196,4 +199,4 @@ export type TUpsertSecretScanningConfigDTO = { content: string | null; }; -export type TSecretScanningDataSourceCredentials = undefined; +export type TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts index b030273deb..bfac2b5315 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts @@ -1,14 +1,14 @@ import { z } from "zod"; -import { BitBucketDataSourceSchema, BitBucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; +import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github"; export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [ GitHubDataSourceSchema, - BitBucketDataSourceSchema + BitbucketDataSourceSchema ]); export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [ GitHubFindingSchema, - BitBucketFindingSchema + BitbucketFindingSchema ]); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e5ed91a263..145a50b232 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2270,8 +2270,8 @@ export const AppConnections = { accessTokenType: "The type of token used to connect with GitLab." }, BITBUCKET: { - email: "The email used to access BitBucket.", - apiToken: "The API token used to access BitBucket." + email: "The email used to access Bitbucket.", + apiToken: "The API token used to access Bitbucket." } } }; @@ -2634,6 +2634,7 @@ export const SecretScanningDataSources = { includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' }, BITBUCKET: { + workspaceSlug: "The workspace to scan.", includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' } } diff --git a/backend/src/server/plugins/secret-scanner-v2.ts b/backend/src/server/plugins/secret-scanner-v2.ts index 6b47e324b4..4c35adcbb6 100644 --- a/backend/src/server/plugins/secret-scanner-v2.ts +++ b/backend/src/server/plugins/secret-scanner-v2.ts @@ -1,7 +1,9 @@ import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types"; import { PushEvent } from "@octokit/webhooks-types"; import { Probot } from "probot"; +import { z } from "zod"; +import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { writeLimit } from "@app/server/config/rateLimiter"; @@ -64,5 +66,31 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide } }); - // TODO(andrey): Register a webhook for BitBucket + // bitbucket push event webhook + server.route({ + method: "POST", + url: "/bitbucket", + schema: { + querystring: z.object({ + dataSourceId: z.string().min(1, { message: "Data Source ID is required" }) + }) + }, + config: { + rateLimit: writeLimit + }, + handler: async (req, res) => { + // TODO(andrey): Verify IP is from bitbucket + + const { dataSourceId } = req.query; + + if (!dataSourceId) return res.status(400).send({ message: "Data Source ID is required" }); + + await server.services.secretScanningV2.bitbucket.handlePushEvent({ + ...(req.body as TBitbucketPushEvent), + dataSourceId + }); + + return res.send("ok"); + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index be991703c2..8767c0680a 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -32,8 +32,8 @@ import { SanitizedAzureKeyVaultConnectionSchema } from "@app/services/app-connection/azure-key-vault"; import { - BitBucketConnectionListItemSchema, - SanitizedBitBucketConnectionSchema + BitbucketConnectionListItemSchema, + SanitizedBitbucketConnectionSchema } from "@app/services/app-connection/bitbucket"; import { CamundaConnectionListItemSchema, @@ -121,7 +121,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedFlyioConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options, - ...SanitizedBitBucketConnectionSchema.options + ...SanitizedBitbucketConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -154,7 +154,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ FlyioConnectionListItemSchema, GitLabConnectionListItemSchema, CloudflareConnectionListItemSchema, - BitBucketConnectionListItemSchema + BitbucketConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts index 4f4271273a..253002e763 100644 --- a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -4,25 +4,53 @@ import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { - CreateBitBucketConnectionSchema, - SanitizedBitBucketConnectionSchema, - UpdateBitBucketConnectionSchema + CreateBitbucketConnectionSchema, + SanitizedBitbucketConnectionSchema, + UpdateBitbucketConnectionSchema } from "@app/services/app-connection/bitbucket"; import { AuthMode } from "@app/services/auth/auth-type"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; -export const registerBitBucketConnectionRouter = async (server: FastifyZodProvider) => { +export const registerBitbucketConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ - app: AppConnection.BitBucket, + app: AppConnection.Bitbucket, server, - sanitizedResponseSchema: SanitizedBitBucketConnectionSchema, - createSchema: CreateBitBucketConnectionSchema, - updateSchema: UpdateBitBucketConnectionSchema + sanitizedResponseSchema: SanitizedBitbucketConnectionSchema, + createSchema: CreateBitbucketConnectionSchema, + updateSchema: UpdateBitbucketConnectionSchema }); // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/workspaces`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + workspaces: z.object({ slug: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId } + } = req; + + const workspaces = await server.services.appConnection.bitbucket.listWorkspaces(connectionId, req.permission); + + return { workspaces }; + } + }); + server.route({ method: "GET", url: `/:connectionId/repositories`, @@ -33,17 +61,26 @@ export const registerBitBucketConnectionRouter = async (server: FastifyZodProvid params: z.object({ connectionId: z.string().uuid() }), + querystring: z.object({ + workspaceSlug: z.string() + }), response: { 200: z.object({ - repositories: z.object({ id: z.string(), name: z.string() }).array() + repositories: z.object({ slug: z.string(), full_name: z.string() }).array() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { connectionId } = req.params; + const { + params: { connectionId }, + query: { workspaceSlug } + } = req; - const repositories = await server.services.appConnection.bitbucket.listRepositories(connectionId, req.permission); + const repositories = await server.services.appConnection.bitbucket.listRepositories( + { connectionId, workspaceSlug }, + req.permission + ); return { repositories }; } diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index e164a90882..0d33afb346 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -9,7 +9,7 @@ import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-confi import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router"; import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; -import { registerBitBucketConnectionRouter } from "./bitbucket-connection-router"; +import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; @@ -64,5 +64,5 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { getFlyioConnectionListItem(), getGitLabConnectionListItem(), getCloudflareConnectionListItem(), - getBitBucketConnectionListItem() + getBitbucketConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -223,7 +223,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.BitBucket]: validateBitBucketConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -260,7 +260,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.APIToken: - case BitBucketConnectionMethod.ApiToken: + case BitbucketConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -341,7 +341,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, - [AppConnection.BitBucket]: platformManagedCredentialsNotSupported + [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index a2d953d07a..cb4351fc75 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -30,7 +30,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", [AppConnection.Cloudflare]: "Cloudflare", - [AppConnection.BitBucket]: "BitBucket" + [AppConnection.Bitbucket]: "Bitbucket" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -63,5 +63,5 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -239,7 +239,7 @@ export type TAppConnectionInput = { id: string } & ( | TFlyioConnectionInput | TGitLabConnectionInput | TCloudflareConnectionInput - | TBitBucketConnectionInput + | TBitbucketConnectionInput ); export type TSqlConnectionInput = @@ -284,7 +284,7 @@ export type TAppConnectionConfig = | TFlyioConnectionConfig | TGitLabConnectionConfig | TCloudflareConnectionConfig - | TBitBucketConnectionConfig; + | TBitbucketConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -316,7 +316,7 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateFlyioConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema - | TValidateBitBucketConnectionCredentialsSchema; + | TValidateBitbucketConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts index 629c5b6d72..0379158638 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts @@ -1,3 +1,3 @@ -export enum BitBucketConnectionMethod { +export enum BitbucketConnectionMethod { ApiToken = "api-token" } diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts index 5c82365c4c..14159fa497 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -5,18 +5,23 @@ import { BadRequestError } from "@app/lib/errors"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; -import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; -import { TBitBucketConnection, TBitBucketConnectionConfig, TBitBucketRepo } from "./bitbucket-connection-types"; +import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; +import { + TBitbucketConnection, + TBitbucketConnectionConfig, + TBitbucketRepo, + TBitbucketWorkspace +} from "./bitbucket-connection-types"; -export const getBitBucketConnectionListItem = () => { +export const getBitbucketConnectionListItem = () => { return { - name: "BitBucket" as const, - app: AppConnection.BitBucket as const, - methods: Object.values(BitBucketConnectionMethod) as [BitBucketConnectionMethod.ApiToken] + name: "Bitbucket" as const, + app: AppConnection.Bitbucket as const, + methods: Object.values(BitbucketConnectionMethod) as [BitbucketConnectionMethod.ApiToken] }; }; -export const getBitBucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => { +export const getBitbucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => { try { const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, { headers: { @@ -38,12 +43,17 @@ export const getBitBucketUser = async ({ email, apiToken }: { email: string; api } }; -export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => { - await getBitBucketUser(config.credentials); +export const validateBitbucketConnectionCredentials = async (config: TBitbucketConnectionConfig) => { + await getBitbucketUser(config.credentials); return config.credentials; }; -export const listBitBucketRepositories = async (appConnection: TBitBucketConnection) => { +interface BitbucketWorkspacesResponse { + values: TBitbucketWorkspace[]; + next?: string; +} + +export const listBitbucketWorkspaces = async (appConnection: TBitbucketConnection) => { const { email, apiToken } = appConnection.credentials; const headers = { @@ -51,19 +61,52 @@ export const listBitBucketRepositories = async (appConnection: TBitBucketConnect Accept: "application/json" }; - let allRepos: TBitBucketRepo[] = []; - let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`; + let allWorkspaces: TBitbucketWorkspace[] = []; + let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces?pagelen=100`; + let iterationCount = 0; + + // Limit to 10 iterations, fetching at most 10 * 100 = 1000 workspaces + while (nextUrl && iterationCount < 10) { + // eslint-disable-next-line no-await-in-loop + const { data }: { data: BitbucketWorkspacesResponse } = await request.get(nextUrl, { + headers + }); + + allWorkspaces = allWorkspaces.concat(data.values.map((workspace) => ({ slug: workspace.slug }))); + nextUrl = data.next; + iterationCount += 1; + } + + return allWorkspaces; +}; + +interface BitbucketRepositoriesResponse { + values: TBitbucketRepo[]; + next?: string; +} + +export const listBitbucketRepositories = async (appConnection: TBitbucketConnection, workspaceSlug: string) => { + const { email, apiToken } = appConnection.credentials; + + const headers = { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + }; + + let allRepos: TBitbucketRepo[] = []; + let nextUrl: string | undefined = + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}?pagelen=100`; let iterationCount = 0; // Limit to 10 iterations, fetching at most 10 * 100 = 1000 repositories while (nextUrl && iterationCount < 10) { // eslint-disable-next-line no-await-in-loop - const { data }: { data: { values: TBitBucketRepo[]; next?: string } } = await request.get<{ - values: TBitBucketRepo[]; - next?: string; - }>(nextUrl, { - headers - }); + const { data }: { data: BitbucketRepositoriesResponse } = await request.get( + nextUrl, + { + headers + } + ); allRepos = allRepos.concat(data.values); nextUrl = data.next; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts index a331db8cc2..fce641c5d6 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts @@ -8,54 +8,54 @@ import { GenericUpdateAppConnectionFieldsSchema } from "@app/services/app-connection/app-connection-schemas"; -import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; +import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; -export const BitBucketConnectionAccessTokenCredentialsSchema = z.object({ +export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({ apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.BITBUCKET.apiToken), email: z.string().email().trim().min(1, "Email required").describe(AppConnections.CREDENTIALS.BITBUCKET.email) }); -const BaseBitBucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.BitBucket) }); +const BaseBitbucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Bitbucket) }); -export const BitBucketConnectionSchema = BaseBitBucketConnectionSchema.extend({ - method: z.literal(BitBucketConnectionMethod.ApiToken), - credentials: BitBucketConnectionAccessTokenCredentialsSchema +export const BitbucketConnectionSchema = BaseBitbucketConnectionSchema.extend({ + method: z.literal(BitbucketConnectionMethod.ApiToken), + credentials: BitbucketConnectionAccessTokenCredentialsSchema }); -export const SanitizedBitBucketConnectionSchema = z.discriminatedUnion("method", [ - BaseBitBucketConnectionSchema.extend({ - method: z.literal(BitBucketConnectionMethod.ApiToken), - credentials: BitBucketConnectionAccessTokenCredentialsSchema.pick({ +export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method", [ + BaseBitbucketConnectionSchema.extend({ + method: z.literal(BitbucketConnectionMethod.ApiToken), + credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({ email: true }) }) ]); -export const ValidateBitBucketConnectionCredentialsSchema = z.discriminatedUnion("method", [ +export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [ z.object({ method: z - .literal(BitBucketConnectionMethod.ApiToken) - .describe(AppConnections.CREATE(AppConnection.BitBucket).method), - credentials: BitBucketConnectionAccessTokenCredentialsSchema.describe( - AppConnections.CREATE(AppConnection.BitBucket).credentials + .literal(BitbucketConnectionMethod.ApiToken) + .describe(AppConnections.CREATE(AppConnection.Bitbucket).method), + credentials: BitbucketConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Bitbucket).credentials ) }) ]); -export const CreateBitBucketConnectionSchema = ValidateBitBucketConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.BitBucket) +export const CreateBitbucketConnectionSchema = ValidateBitbucketConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Bitbucket) ); -export const UpdateBitBucketConnectionSchema = z +export const UpdateBitbucketConnectionSchema = z .object({ - credentials: BitBucketConnectionAccessTokenCredentialsSchema.optional().describe( - AppConnections.UPDATE(AppConnection.BitBucket).credentials + credentials: BitbucketConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Bitbucket).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.BitBucket)); + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket)); -export const BitBucketConnectionListItemSchema = z.object({ - name: z.literal("BitBucket"), - app: z.literal(AppConnection.BitBucket), - methods: z.nativeEnum(BitBucketConnectionMethod).array() +export const BitbucketConnectionListItemSchema = z.object({ + name: z.literal("Bitbucket"), + app: z.literal(AppConnection.Bitbucket), + methods: z.nativeEnum(BitbucketConnectionMethod).array() }); diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts index 008797823d..290ac21e3d 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -1,25 +1,33 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { listBitBucketRepositories } from "./bitbucket-connection-fns"; -import { TBitBucketConnection } from "./bitbucket-connection-types"; +import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns"; +import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, connectionId: string, actor: OrgServiceActor -) => Promise; +) => Promise; export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => { - const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { - const appConnection = await getAppConnection(AppConnection.BitBucket, connectionId, actor); + const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const workspaces = await listBitbucketWorkspaces(appConnection); + return workspaces; + }; - const repositories = await listBitBucketRepositories(appConnection); - - return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name })); + const listRepositories = async ( + { connectionId, workspaceSlug }: TGetBitbucketRepositoriesDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const repositories = await listBitbucketRepositories(appConnection, workspaceSlug); + return repositories; }; return { + listWorkspaces, listRepositories }; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts index 41fdaef297..53e93b4793 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -4,27 +4,36 @@ import { DiscriminativePick } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; import { - BitBucketConnectionSchema, - CreateBitBucketConnectionSchema, - ValidateBitBucketConnectionCredentialsSchema + BitbucketConnectionSchema, + CreateBitbucketConnectionSchema, + ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket-connection-schemas"; -export type TBitBucketConnection = z.infer; +export type TBitbucketConnection = z.infer; -export type TBitBucketConnectionInput = z.infer & { - app: AppConnection.BitBucket; +export type TBitbucketConnectionInput = z.infer & { + app: AppConnection.Bitbucket; }; -export type TValidateBitBucketConnectionCredentialsSchema = typeof ValidateBitBucketConnectionCredentialsSchema; +export type TValidateBitbucketConnectionCredentialsSchema = typeof ValidateBitbucketConnectionCredentialsSchema; -export type TBitBucketConnectionConfig = DiscriminativePick< - TBitBucketConnectionInput, +export type TBitbucketConnectionConfig = DiscriminativePick< + TBitbucketConnectionInput, "method" | "app" | "credentials" > & { orgId: string; }; -export type TBitBucketRepo = { +export type TGetBitbucketRepositoriesDTO = { + connectionId: string; + workspaceSlug: string; +}; + +export type TBitbucketWorkspace = { + slug: string; +}; + +export type TBitbucketRepo = { full_name: string; // workspace-slug/repo-slug slug: string; }; diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 4a5fd8231a..c549d12658 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -814,9 +814,9 @@ const getAppsCloudflareWorkers = async ({ accessToken, accountId }: { accessToke }; /** - * Return list of repositories for the BitBucket integration based on provided BitBucket workspace + * Return list of repositories for the Bitbucket integration based on provided Bitbucket workspace */ -const getAppsBitBucket = async ({ accessToken, workspaceSlug }: { accessToken: string; workspaceSlug?: string }) => { +const getAppsBitbucket = async ({ accessToken, workspaceSlug }: { accessToken: string; workspaceSlug?: string }) => { interface RepositoriesResponse { size: number; page: number; @@ -1302,7 +1302,7 @@ export const getApps = async ({ }); case Integrations.BITBUCKET: - return getAppsBitBucket({ + return getAppsBitbucket({ accessToken, workspaceSlug }); diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 9b9841f1fd..0608bbd4b7 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -342,7 +342,7 @@ export const getIntegrationOptions = async () => { { name: "Bitbucket", slug: "bitbucket", - image: "BitBucket.png", + image: "Bitbucket.png", isAvailable: true, type: "oauth", clientId: appCfg.CLIENT_ID_BITBUCKET, diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 989a5a88ca..1cd4569acc 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -3921,9 +3921,9 @@ const syncSecretsCloudflareWorkers = async ({ }; /** - * Sync/push [secrets] to BitBucket repo with name [integration.app] + * Sync/push [secrets] to Bitbucket repo with name [integration.app] */ -const syncSecretsBitBucket = async ({ +const syncSecretsBitbucket = async ({ integration, secrets, accessToken @@ -4832,7 +4832,7 @@ export const syncIntegrationSecrets = async ({ }); break; case Integrations.BITBUCKET: - await syncSecretsBitBucket({ + await syncSecretsBitbucket({ integration, secrets, accessToken diff --git a/backend/src/services/integration-auth/integration-token.ts b/backend/src/services/integration-auth/integration-token.ts index 362b20a07d..a15c9dd1f3 100644 --- a/backend/src/services/integration-auth/integration-token.ts +++ b/backend/src/services/integration-auth/integration-token.ts @@ -64,7 +64,7 @@ type ExchangeCodeGitlabResponse = { created_at: number; }; -type ExchangeCodeBitBucketResponse = { +type ExchangeCodeBitbucketResponse = { access_token: string; token_type: string; expires_in: number; @@ -392,10 +392,10 @@ const exchangeCodeGitlab = async ({ code, url }: { code: string; url?: string }) }; /** - * Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Bitbucket * code-token exchange */ -const exchangeCodeBitBucket = async ({ code }: { code: string }) => { +const exchangeCodeBitbucket = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); const appCfg = getConfig(); if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) { @@ -403,7 +403,7 @@ const exchangeCodeBitBucket = async ({ code }: { code: string }) => { } const res = ( - await request.post( + await request.post( IntegrationUrls.BITBUCKET_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", @@ -490,7 +490,7 @@ export const exchangeCode = async ({ url }); case Integrations.BITBUCKET: - return exchangeCodeBitBucket({ + return exchangeCodeBitbucket({ code }); default: @@ -524,7 +524,7 @@ type RefreshTokenGitLabResponse = { created_at: number; }; -type RefreshTokenBitBucketResponse = { +type RefreshTokenBitbucketResponse = { access_token: string; token_type: string; expires_in: number; @@ -653,9 +653,9 @@ const exchangeRefreshGitLab = async ({ refreshToken, url }: { url?: string | nul /** * Return new access token by exchanging refresh token [refreshToken] for the - * BitBucket integration + * Bitbucket integration */ -const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string }) => { +const exchangeRefreshBitbucket = async ({ refreshToken }: { refreshToken: string }) => { const accessExpiresAt = new Date(); const appCfg = getConfig(); if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) { @@ -664,7 +664,7 @@ const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string const { data }: { - data: RefreshTokenBitBucketResponse; + data: RefreshTokenBitbucketResponse; } = await request.post( IntegrationUrls.BITBUCKET_TOKEN_URL, new URLSearchParams({ @@ -794,7 +794,7 @@ export const exchangeRefresh = async ( url }); case Integrations.BITBUCKET: - return exchangeRefreshBitBucket({ + return exchangeRefreshBitbucket({ refreshToken }); case Integrations.GCP_SECRET_MANAGER: diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 08debf8cf0..5dc06daed9 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -35,7 +35,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | | [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | | [Windmill](/integrations/cloud/windmill) | Cloud | Available | -| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available | +| [Bitbucket](/integrations/cicd/bitbucket) | CI/CD | Available | | [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available | | [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | | [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 90f3b22076..29f5d8f157 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -669,11 +669,11 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I - OAuth2 client ID for BitBucket integration + OAuth2 client ID for Bitbucket integration - OAuth2 client secret for BitBucket integration + OAuth2 client secret for Bitbucket integration diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx index f215e41ca7..98369f2c47 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx @@ -1,13 +1,15 @@ import { useEffect } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -import { MultiValue } from "react-select"; +import { MultiValue, SingleValue } from "react-select"; import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; import { - TBitBucketRepo, - useBitBucketConnectionListRepositories + TBitbucketRepo, + TBitbucketWorkspace, + useBitbucketConnectionListRepositories, + useBitbucketConnectionListWorkspaces } from "@app/hooks/api/appConnections/bitbucket"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; @@ -19,18 +21,25 @@ enum ScanMethod { SelectRepositories = "select-repositories" } -export const BitBucketDataSourceConfigFields = () => { +export const BitbucketDataSourceConfigFields = () => { const { control, watch, setValue } = useFormContext< TSecretScanningDataSourceForm & { - type: SecretScanningDataSource.BitBucket; + type: SecretScanningDataSource.Bitbucket; } >(); const connectionId = useWatch({ control, name: "connection.id" }); const isUpdate = Boolean(watch("id")); + const selectedWorkspaceSlug = useWatch({ control, name: "config.workspaceSlug" }); + + const { data: workspaces, isPending: areWorkspacesLoading } = + useBitbucketConnectionListWorkspaces(connectionId, { enabled: Boolean(connectionId) }); + const { data: repositories, isPending: areRepositoriesLoading } = - useBitBucketConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) }); + useBitbucketConnectionListRepositories(connectionId, selectedWorkspaceSlug, { + enabled: Boolean(connectionId) && Boolean(selectedWorkspaceSlug) // Enable only if both are present + }); const includeRepos = watch("config.includeRepos"); @@ -51,10 +60,50 @@ export const BitBucketDataSourceConfigFields = () => { isUpdate={isUpdate} onChange={() => { if (scanMethod === ScanMethod.SelectRepositories) { + setValue("config.workspaceSlug", ""); setValue("config.includeRepos", []); } }} /> + ( + Ensure that your connection has the correct permissions.} + > +
+ Don't see the workspaces you're looking for?{" "} + +
+ + } + > + { + onChange((newValue as SingleValue)?.slug); + if (scanMethod === ScanMethod.SelectRepositories) { + setValue("config.includeRepos", []); + } + }} + options={workspaces} + placeholder="Select workspace..." + getOptionLabel={(option) => option.slug} + getOptionValue={(option) => option.slug} + /> +
+ )} + />