Make webhooks work, add workspace selection, rename BitBucket to

Bitbucket
This commit is contained in:
x032205
2025-07-03 18:49:29 -04:00
parent 8c6b903204
commit 6a6f08fc4d
61 changed files with 789 additions and 444 deletions

View File

@@ -1,16 +1,16 @@
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints"; import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
import { import {
BitBucketDataSourceSchema, BitbucketDataSourceSchema,
CreateBitBucketDataSourceSchema, CreateBitbucketDataSourceSchema,
UpdateBitBucketDataSourceSchema UpdateBitbucketDataSourceSchema
} from "@app/ee/services/secret-scanning-v2/bitbucket"; } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; 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({ registerSecretScanningEndpoints({
type: SecretScanningDataSource.BitBucket, type: SecretScanningDataSource.Bitbucket,
server, server,
responseSchema: BitBucketDataSourceSchema, responseSchema: BitbucketDataSourceSchema,
createSchema: CreateBitBucketDataSourceSchema, createSchema: CreateBitbucketDataSourceSchema,
updateSchema: UpdateBitBucketDataSourceSchema updateSchema: UpdateBitbucketDataSourceSchema
}); });

View File

@@ -1,6 +1,6 @@
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; 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"; import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router";
export * from "./secret-scanning-v2-router"; export * from "./secret-scanning-v2-router";
@@ -10,5 +10,5 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
(server: FastifyZodProvider) => Promise<void> (server: FastifyZodProvider) => Promise<void>
> = { > = {
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter, [SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
[SecretScanningDataSource.BitBucket]: registerBitBucketSecretScanningRouter [SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter
}; };

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { SecretScanningConfigsSchema } from "@app/db/schemas"; import { SecretScanningConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; 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 { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
import { import {
SecretScanningFindingStatus, SecretScanningFindingStatus,
@@ -24,7 +24,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [ const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
GitHubDataSourceListItemSchema, GitHubDataSourceListItemSchema,
BitBucketDataSourceListItemSchema BitbucketDataSourceListItemSchema
]); ]);
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => { export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {

View File

@@ -18,33 +18,33 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0, environmentsUsed: 0,
identityLimit: null, identityLimit: null,
identitiesUsed: 0, identitiesUsed: 0,
dynamicSecret: false, dynamicSecret: true,
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: true,
ipAllowlisting: false, ipAllowlisting: true,
rbac: false, rbac: true,
githubOrgSync: false, githubOrgSync: true,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: true,
secretAccessInsights: false, secretAccessInsights: true,
auditLogs: false, auditLogs: true,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 3,
auditLogStreams: false, auditLogStreams: true,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: false, samlSSO: true,
hsm: false, hsm: true,
oidcSSO: false, oidcSSO: true,
scim: false, scim: true,
ldap: false, ldap: true,
groups: false, groups: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
secretApproval: false, secretApproval: true,
secretRotation: false, secretRotation: true,
caCrl: false, caCrl: true,
instanceUserManagement: false, instanceUserManagement: true,
externalKms: false, externalKms: true,
rateLimits: { rateLimits: {
readLimit: 60, readLimit: 60,
writeLimit: 200, writeLimit: 200,
@@ -52,13 +52,13 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
}, },
pkiEst: false, pkiEst: false,
enforceMfa: false, enforceMfa: false,
projectTemplates: false, projectTemplates: true,
kmip: false, kmip: true,
gateway: false, gateway: true,
sshHostGroups: false, sshHostGroups: true,
secretScanning: false, enterpriseAppConnections: true,
enterpriseSecretSyncs: false, enterpriseSecretSyncs: true,
enterpriseAppConnections: false secretScanning: true
}); });
export const setupLicenseRequestWithStore = ( export const setupLicenseRequestWithStore = (

View File

@@ -3,7 +3,7 @@ import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scann
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = { export const BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = {
name: "BitBucket", name: "Bitbucket",
type: SecretScanningDataSource.BitBucket, type: SecretScanningDataSource.Bitbucket,
connection: AppConnection.BitBucket connection: AppConnection.Bitbucket
}; };

View File

@@ -3,7 +3,6 @@ import { join } from "path";
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; 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 { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { import {
SecretScanningDataSource,
SecretScanningFindingSeverity, SecretScanningFindingSeverity,
SecretScanningResource SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
@@ -20,54 +19,106 @@ import {
TSecretScanningFactoryListRawResources, TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization TSecretScanningFactoryPostInitialization
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types"; } 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 { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { titleCaseToCamelCase } from "@app/lib/fn"; import { titleCaseToCamelCase } from "@app/lib/fn";
import { GitHubRepositoryRegex } from "@app/lib/regex"; import { GitHubRepositoryRegex } from "@app/lib/regex";
import { import {
getBitBucketUser, getBitbucketUser,
listBitBucketRepositories, listBitbucketRepositories,
TBitBucketConnection TBitbucketConnection
} from "@app/services/app-connection/bitbucket"; } 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 = () => { export const BitbucketSecretScanningFactory = () => {
const initialize: TSecretScanningFactoryInitialize<TBitBucketConnection> = async ( const initialize: TSecretScanningFactoryInitialize<TBitbucketConnection> = async (
{ connection, secretScanningV2DAL }, { connection, payload },
callback callback
) => { ) => {
// TODO(andrey): Swap for something proper const { email, apiToken } = connection.credentials;
const externalId = connection.credentials.email; const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
const existingDataSource = await secretScanningV2DAL.dataSources.findOne({ const { data } = await request.post<{ uuid: string }>(
externalId, `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks`,
type: SecretScanningDataSource.BitBucket {
}); description: "Infisical webhook for push events",
url: `https://tunnel.util.lol/secret-scanning/webhooks/bitbucket`, // TODO(andrey): Swap to ${cfg.SITE_URL}
if (existingDataSource) active: true,
throw new BadRequestError({ events: ["repo:push"]
message: `A Data Source already exists for this BitBucket Radar Connection in the Project with ID "${existingDataSource.projectId}"` },
}); {
headers: {
Authorization: authHeader,
Accept: "application/json"
}
}
);
return callback({ return callback({
externalId credentials: { webhookId: data.uuid }
}); });
}; };
const postInitialization: TSecretScanningFactoryPostInitialization<TBitBucketConnection> = async () => { const postInitialization: TSecretScanningFactoryPostInitialization<TBitbucketConnection> = async ({
// no post-initialization required 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<TBitBucketDataSourceWithConnection> = 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<TBitbucketDataSourceWithConnection> = async (
dataSource dataSource
) => { ) => {
const { const {
connection, connection,
config: { includeRepos } config: { includeRepos, workspaceSlug }
} = dataSource; } = dataSource;
const repos = await listBitBucketRepositories(connection); const repos = await listBitbucketRepositories(connection, workspaceSlug);
const filteredRepos: typeof repos = []; const filteredRepos: typeof repos = [];
if (includeRepos.includes("*")) { if (includeRepos.includes("*")) {
@@ -83,7 +134,7 @@ export const BitBucketSecretScanningFactory = () => {
})); }));
}; };
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TBitBucketDataSourceWithConnection> = async ({ const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TBitbucketDataSourceWithConnection> = async ({
dataSource, dataSource,
resourceName, resourceName,
tempFolder tempFolder
@@ -97,10 +148,10 @@ export const BitBucketSecretScanningFactory = () => {
const repoPath = join(tempFolder, "repo.git"); const repoPath = join(tempFolder, "repo.git");
if (!GitHubRepositoryRegex.test(resourceName)) { 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({ await cloneRepository({
cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`, cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`,
@@ -111,18 +162,18 @@ export const BitBucketSecretScanningFactory = () => {
}; };
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload< const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
TQueueBitBucketResourceDiffScan["payload"] TQueueBitbucketResourceDiffScan["payload"]
> = ({ repository }) => { > = ({ repository, dataSourceId }) => {
return { return {
name: repository.full_name, name: repository.full_name,
externalId: repository.id.toString(), externalId: dataSourceId,
type: SecretScanningResource.Repository type: SecretScanningResource.Repository
}; };
}; };
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload< const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
TBitBucketDataSourceWithConnection, TBitbucketDataSourceWithConnection,
TQueueBitBucketResourceDiffScan["payload"] TQueueBitbucketResourceDiffScan["payload"]
> = async ({ dataSource, payload, resourceName, configPath }) => { > = async ({ dataSource, payload, resourceName, configPath }) => {
const { const {
connection: { connection: {
@@ -130,81 +181,86 @@ export const BitBucketSecretScanningFactory = () => {
} }
} = dataSource; } = dataSource;
const { commits, repository } = payload; const { push, repository } = payload;
const allFindings: SecretMatch[] = []; const allFindings: SecretMatch[] = [];
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
for (const commit of commits) { for (const change of push.changes) {
// eslint-disable-next-line no-await-in-loop for (const commit of change.commits) {
const { data: diffstat } = await request.get<{ // eslint-disable-next-line no-await-in-loop
values: { const { data: diffstat } = await request.get<{
status: "added" | "modified" | "removed" | "renamed"; values: {
new?: { path: string }; status: "added" | "modified" | "removed" | "renamed";
old?: { path: string }; new?: { path: string };
}[]; old?: { path: string };
}>(`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diffstat/${commit.id}`, { }[];
headers: { }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${repository.full_name}/diffstat/${commit.hash}`, {
Authorization: authHeader, headers: {
Accept: "application/json" Authorization: authHeader,
} Accept: "application/json"
}); }
});
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!diffstat.values) continue; if (!diffstat.values) continue;
for (const file of diffstat.values) { for (const file of diffstat.values) {
if ((file.status === "added" || file.status === "modified") && file.new?.path) { if ((file.status === "added" || file.status === "modified") && file.new?.path) {
const filePath = file.new.path; const filePath = file.new.path;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const { data: patch } = await request.get<string>( const { data: patch } = await request.get<string>(
`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.id}`, `https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.hash}`,
{ {
params: { params: {
path: filePath path: filePath
}, },
headers: { headers: {
Authorization: authHeader Authorization: authHeader
}, },
responseType: "text" responseType: "text"
} }
); );
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!patch) continue; if (!patch) continue;
// eslint-disable-next-line // eslint-disable-next-line no-await-in-loop
const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath);
const adjustedFindings = findings.map((finding) => { const adjustedFindings = findings.map((finding) => {
const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine); const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine);
const endLine = const endLine =
finding.StartLine === finding.EndLine finding.StartLine === finding.EndLine
? startLine ? startLine
: convertPatchLineToFileLineNumber(patch, finding.EndLine); : convertPatchLineToFileLineNumber(patch, finding.EndLine);
const startColumn = finding.StartColumn - 1; // subtract 1 for + const startColumn = finding.StartColumn - 1; // subtract 1 for +
const endColumn = finding.EndColumn - 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 { return {
...finding, ...finding,
StartLine: startLine, StartLine: startLine,
EndLine: endLine, EndLine: endLine,
StartColumn: startColumn, StartColumn: startColumn,
EndColumn: endColumn, EndColumn: endColumn,
File: filePath, File: filePath,
Commit: commit.id, Commit: commit.hash,
Author: commit.author.name, Author: authorName,
Email: commit.author.email ?? "", Email: authorEmail,
Message: commit.message, Message: commit.message,
Fingerprint: `${commit.id}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`, Fingerprint: `${commit.hash}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`,
Date: commit.timestamp, Date: commit.date,
Link: `https://bitbucket.org/${resourceName}/src/${commit.id}/${filePath}#lines-${startLine}` Link: `https://bitbucket.org/${resourceName}/src/${commit.hash}/${filePath}#lines-${startLine}`
}; };
}); });
allFindings.push(...adjustedFindings); allFindings.push(...adjustedFindings);
}
} }
} }
} }

View File

@@ -15,7 +15,12 @@ import { SecretScanningDataSources } from "@app/lib/api-docs";
import { GitHubRepositoryRegex } from "@app/lib/regex"; import { GitHubRepositoryRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; 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 includeRepos: z
.array( .array(
z z
@@ -30,58 +35,62 @@ export const BitBucketDataSourceConfigSchema = z.object({
.describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos) .describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos)
}); });
export const BitBucketDataSourceSchema = BaseSecretScanningDataSourceSchema({ export const BitbucketDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.BitBucket, type: SecretScanningDataSource.Bitbucket,
isConnectionRequired: true isConnectionRequired: true
}) })
.extend({ .extend({
config: BitBucketDataSourceConfigSchema config: BitbucketDataSourceConfigSchema
}) })
.describe( .describe(
JSON.stringify({ JSON.stringify({
title: "BitBucket" title: "Bitbucket"
}) })
); );
export const CreateBitBucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({ export const CreateBitbucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.BitBucket, type: SecretScanningDataSource.Bitbucket,
isConnectionRequired: true isConnectionRequired: true
}) })
.extend({ .extend({
config: BitBucketDataSourceConfigSchema config: BitbucketDataSourceConfigSchema
}) })
.describe( .describe(
JSON.stringify({ JSON.stringify({
title: "BitBucket" title: "Bitbucket"
}) })
); );
export const UpdateBitBucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema( export const UpdateBitbucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(
SecretScanningDataSource.BitBucket SecretScanningDataSource.Bitbucket
) )
.extend({ .extend({
config: BitBucketDataSourceConfigSchema.optional() config: BitbucketDataSourceConfigSchema.optional()
}) })
.describe( .describe(
JSON.stringify({ JSON.stringify({
title: "BitBucket" title: "Bitbucket"
}) })
); );
export const BitBucketDataSourceListItemSchema = z export const BitbucketDataSourceListItemSchema = z
.object({ .object({
name: z.literal("BitBucket"), name: z.literal("Bitbucket"),
connection: z.literal(AppConnection.BitBucket), connection: z.literal(AppConnection.Bitbucket),
type: z.literal(SecretScanningDataSource.BitBucket) type: z.literal(SecretScanningDataSource.Bitbucket)
}) })
.describe( .describe(
JSON.stringify({ JSON.stringify({
title: "BitBucket" title: "Bitbucket"
}) })
); );
export const BitBucketFindingSchema = BaseSecretScanningFindingSchema.extend({ export const BitbucketFindingSchema = BaseSecretScanningFindingSchema.extend({
resourceType: z.literal(SecretScanningResource.Repository), resourceType: z.literal(SecretScanningResource.Repository),
dataSourceType: z.literal(SecretScanningDataSource.BitBucket), dataSourceType: z.literal(SecretScanningDataSource.Bitbucket),
details: GitRepositoryScanFindingDetailsSchema details: GitRepositoryScanFindingDetailsSchema
}); });
export const BitbucketDataSourceCredentialsSchema = z.object({
webhookId: z.string()
});

View File

@@ -1,11 +1,9 @@
import { PushEvent } from "@octokit/webhooks-types";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal"; 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 { 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 { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TBitBucketDataSource } from "./bitbucket-secret-scanning-types"; import { TBitbucketDataSource, TBitbucketPushEvent } from "./bitbucket-secret-scanning-types";
export const bitBucketSecretScanningService = ( export const bitBucketSecretScanningService = (
secretScanningV2DAL: TSecretScanningV2DALFactory, secretScanningV2DAL: TSecretScanningV2DALFactory,
@@ -14,18 +12,18 @@ export const bitBucketSecretScanningService = (
const handleInstallationDeletedEvent = async (installationId: number) => { const handleInstallationDeletedEvent = async (installationId: number) => {
const dataSource = await secretScanningV2DAL.dataSources.findOne({ const dataSource = await secretScanningV2DAL.dataSources.findOne({
externalId: String(installationId), externalId: String(installationId),
type: SecretScanningDataSource.BitBucket type: SecretScanningDataSource.Bitbucket
}); });
if (!dataSource) { if (!dataSource) {
logger.error( logger.error(
`secretScanningV2RemoveEvent: BitBucket - Could not find data source [installationId=${installationId}]` `secretScanningV2RemoveEvent: Bitbucket - Could not find data source [installationId=${installationId}]`
); );
return; return;
} }
logger.info( 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, { await secretScanningV2DAL.dataSources.updateById(dataSource.id, {
@@ -33,24 +31,26 @@ export const bitBucketSecretScanningService = (
}); });
}; };
const handlePushEvent = async (payload: PushEvent) => { const handlePushEvent = async (payload: TBitbucketPushEvent & { dataSourceId: string }) => {
const { commits, repository, installation } = payload; const { push, repository } = payload;
if (!commits || !repository || !installation) { if (!push?.changes?.length || !repository?.workspace?.uuid) {
logger.warn( 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; return;
} }
const dataSource = (await secretScanningV2DAL.dataSources.findOne({ const dataSource = (await secretScanningV2DAL.dataSources.findOne({
externalId: String(installation.id), externalId: payload.dataSourceId,
type: SecretScanningDataSource.BitBucket type: SecretScanningDataSource.Bitbucket
})) as TBitBucketDataSource | undefined; })) as TBitbucketDataSource | undefined;
if (!dataSource) { if (!dataSource) {
logger.error( logger.error(
`secretScanningV2PushEvent: BitBucket - Could not find data source [installationId=${installation.id}]` `secretScanningV2PushEvent: Bitbucket - Could not find data source [workspaceUuid=${repository.workspace.uuid}]`
); );
return; return;
} }
@@ -62,20 +62,20 @@ export const bitBucketSecretScanningService = (
if (!isAutoScanEnabled) { if (!isAutoScanEnabled) {
logger.info( 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; return;
} }
if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) { if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) {
await secretScanningV2Queue.queueResourceDiffScan({ await secretScanningV2Queue.queueResourceDiffScan({
dataSourceType: SecretScanningDataSource.BitBucket, dataSourceType: SecretScanningDataSource.Bitbucket,
payload, payload,
dataSourceId: dataSource.id dataSourceId: dataSource.id
}); });
} else { } else {
logger.info( 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}]`
); );
} }
}; };

View File

@@ -1,31 +1,84 @@
import { PushEvent } from "@octokit/webhooks-types";
import { z } from "zod"; import { z } from "zod";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; 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 { import {
BitBucketDataSourceListItemSchema, BitbucketDataSourceCredentialsSchema,
BitBucketDataSourceSchema, BitbucketDataSourceListItemSchema,
BitBucketFindingSchema, BitbucketDataSourceSchema,
CreateBitBucketDataSourceSchema BitbucketFindingSchema,
CreateBitbucketDataSourceSchema
} from "./bitbucket-secret-scanning-schemas"; } from "./bitbucket-secret-scanning-schemas";
export type TBitBucketDataSource = z.infer<typeof BitBucketDataSourceSchema>; export type TBitbucketDataSource = z.infer<typeof BitbucketDataSourceSchema>;
export type TBitBucketDataSourceInput = z.infer<typeof CreateBitBucketDataSourceSchema>; export type TBitbucketDataSourceInput = z.infer<typeof CreateBitbucketDataSourceSchema>;
export type TBitBucketDataSourceListItem = z.infer<typeof BitBucketDataSourceListItemSchema>; export type TBitbucketDataSourceListItem = z.infer<typeof BitbucketDataSourceListItemSchema>;
export type TBitBucketFinding = z.infer<typeof BitBucketFindingSchema>; export type TBitbucketDataSourceCredentials = z.infer<typeof BitbucketDataSourceCredentialsSchema>;
export type TBitBucketDataSourceWithConnection = TBitBucketDataSource & { export type TBitbucketFinding = z.infer<typeof BitbucketFindingSchema>;
connection: TBitBucketConnection;
export type TBitbucketDataSourceWithConnection = TBitbucketDataSource & {
connection: TBitbucketConnection;
}; };
export type TQueueBitBucketResourceDiffScan = { export type TBitbucketPushEventRepository = {
dataSourceType: SecretScanningDataSource.BitBucket; full_name: string;
payload: PushEvent; 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; dataSourceId: string;
resourceId: string; resourceId: string;
scanId: string; scanId: string;

View File

@@ -1,6 +1,6 @@
export enum SecretScanningDataSource { export enum SecretScanningDataSource {
GitHub = "github", GitHub = "github",
BitBucket = "bitbucket" Bitbucket = "bitbucket"
} }
export enum SecretScanningScanStatus { export enum SecretScanningScanStatus {

View File

@@ -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 { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
import { SecretScanningDataSource } from "./secret-scanning-v2-enums"; import { SecretScanningDataSource } from "./secret-scanning-v2-enums";
@@ -17,5 +17,5 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory<
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = { export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation, [SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
[SecretScanningDataSource.BitBucket]: BitBucketSecretScanningFactory as TSecretScanningFactoryImplementation [SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation
}; };

View File

@@ -13,7 +13,7 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte
const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = { const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = {
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION, [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 = () => { export const listSecretScanningDataSourceOptions = () => {

View File

@@ -3,15 +3,15 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = { export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = {
[SecretScanningDataSource.GitHub]: "GitHub", [SecretScanningDataSource.GitHub]: "GitHub",
[SecretScanningDataSource.BitBucket]: "BitBucket" [SecretScanningDataSource.Bitbucket]: "Bitbucket"
}; };
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = { export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar, [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.BitBucket]: AppConnection.BitBucket [SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
}; };
export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = { export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }, [SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
[SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" } [SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" }
}; };

View File

@@ -5,12 +5,13 @@ import {
TSecretScanningScans TSecretScanningScans
} from "@app/db/schemas"; } from "@app/db/schemas";
import { import {
TBitBucketDataSource, TBitbucketDataSource,
TBitBucketDataSourceInput, TBitbucketDataSourceCredentials,
TBitBucketDataSourceListItem, TBitbucketDataSourceInput,
TBitBucketDataSourceWithConnection, TBitbucketDataSourceListItem,
TBitBucketFinding, TBitbucketDataSourceWithConnection,
TQueueBitBucketResourceDiffScan TBitbucketFinding,
TQueueBitbucketResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/bitbucket"; } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { import {
TGitHubDataSource, TGitHubDataSource,
@@ -27,7 +28,7 @@ import {
SecretScanningScanStatus SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; } 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 & { export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt?: Date | null; lastScannedAt?: Date | null;
@@ -51,13 +52,15 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
export type TSecretScanningDataSourceWithConnection = export type TSecretScanningDataSourceWithConnection =
| TGitHubDataSourceWithConnection | 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 = { export type TListSecretScanningDataSourcesByProjectId = {
projectId: string; projectId: string;
@@ -109,7 +112,7 @@ export type TQueueSecretScanningDataSourceFullScan = {
scanId: string; scanId: string;
}; };
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitBucketResourceDiffScan; export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan;
export type TQueueSecretScanningSendNotification = { export type TQueueSecretScanningSendNotification = {
dataSource: TSecretScanningDataSources; dataSource: TSecretScanningDataSources;
@@ -149,7 +152,7 @@ export type TSecretScanningDataSourceRaw = NonNullable<
export type TSecretScanningFactoryInitialize< export type TSecretScanningFactoryInitialize<
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
C extends TSecretScanningDataSourceCredentials = undefined C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema
> = ( > = (
params: { params: {
payload: TCreateSecretScanningDataSourceDTO; payload: TCreateSecretScanningDataSourceDTO;
@@ -161,7 +164,7 @@ export type TSecretScanningFactoryInitialize<
export type TSecretScanningFactoryPostInitialization< export type TSecretScanningFactoryPostInitialization<
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
C extends TSecretScanningDataSourceCredentials = undefined C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema
> = (params: { > = (params: {
payload: TCreateSecretScanningDataSourceDTO; payload: TCreateSecretScanningDataSourceDTO;
connection: T; connection: T;
@@ -196,4 +199,4 @@ export type TUpsertSecretScanningConfigDTO = {
content: string | null; content: string | null;
}; };
export type TSecretScanningDataSourceCredentials = undefined; export type TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema;

View File

@@ -1,14 +1,14 @@
import { z } from "zod"; 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"; import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [ export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema, GitHubDataSourceSchema,
BitBucketDataSourceSchema BitbucketDataSourceSchema
]); ]);
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [ export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
GitHubFindingSchema, GitHubFindingSchema,
BitBucketFindingSchema BitbucketFindingSchema
]); ]);

View File

@@ -2270,8 +2270,8 @@ export const AppConnections = {
accessTokenType: "The type of token used to connect with GitLab." accessTokenType: "The type of token used to connect with GitLab."
}, },
BITBUCKET: { BITBUCKET: {
email: "The email used to access BitBucket.", email: "The email used to access Bitbucket.",
apiToken: "The API token 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 (["*"]).' includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
}, },
BITBUCKET: { BITBUCKET: {
workspaceSlug: "The workspace to scan.",
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
} }
} }

View File

@@ -1,7 +1,9 @@
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types"; import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";
import { PushEvent } from "@octokit/webhooks-types"; import { PushEvent } from "@octokit/webhooks-types";
import { Probot } from "probot"; 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 { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { writeLimit } from "@app/server/config/rateLimiter"; 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");
}
});
}; };

View File

@@ -32,8 +32,8 @@ import {
SanitizedAzureKeyVaultConnectionSchema SanitizedAzureKeyVaultConnectionSchema
} from "@app/services/app-connection/azure-key-vault"; } from "@app/services/app-connection/azure-key-vault";
import { import {
BitBucketConnectionListItemSchema, BitbucketConnectionListItemSchema,
SanitizedBitBucketConnectionSchema SanitizedBitbucketConnectionSchema
} from "@app/services/app-connection/bitbucket"; } from "@app/services/app-connection/bitbucket";
import { import {
CamundaConnectionListItemSchema, CamundaConnectionListItemSchema,
@@ -121,7 +121,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedFlyioConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options,
...SanitizedGitLabConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options,
...SanitizedCloudflareConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options,
...SanitizedBitBucketConnectionSchema.options ...SanitizedBitbucketConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -154,7 +154,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
FlyioConnectionListItemSchema, FlyioConnectionListItemSchema,
GitLabConnectionListItemSchema, GitLabConnectionListItemSchema,
CloudflareConnectionListItemSchema, CloudflareConnectionListItemSchema,
BitBucketConnectionListItemSchema BitbucketConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -4,25 +4,53 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { import {
CreateBitBucketConnectionSchema, CreateBitbucketConnectionSchema,
SanitizedBitBucketConnectionSchema, SanitizedBitbucketConnectionSchema,
UpdateBitBucketConnectionSchema UpdateBitbucketConnectionSchema
} from "@app/services/app-connection/bitbucket"; } from "@app/services/app-connection/bitbucket";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerBitBucketConnectionRouter = async (server: FastifyZodProvider) => { export const registerBitbucketConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({ registerAppConnectionEndpoints({
app: AppConnection.BitBucket, app: AppConnection.Bitbucket,
server, server,
sanitizedResponseSchema: SanitizedBitBucketConnectionSchema, sanitizedResponseSchema: SanitizedBitbucketConnectionSchema,
createSchema: CreateBitBucketConnectionSchema, createSchema: CreateBitbucketConnectionSchema,
updateSchema: UpdateBitBucketConnectionSchema updateSchema: UpdateBitbucketConnectionSchema
}); });
// The below endpoints are not exposed and for Infisical App use // 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({ server.route({
method: "GET", method: "GET",
url: `/:connectionId/repositories`, url: `/:connectionId/repositories`,
@@ -33,17 +61,26 @@ export const registerBitBucketConnectionRouter = async (server: FastifyZodProvid
params: z.object({ params: z.object({
connectionId: z.string().uuid() connectionId: z.string().uuid()
}), }),
querystring: z.object({
workspaceSlug: z.string()
}),
response: { response: {
200: z.object({ 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]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { 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 }; return { repositories };
} }

View File

@@ -9,7 +9,7 @@ import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-confi
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router"; import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-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 { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
@@ -64,5 +64,5 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Flyio]: registerFlyioConnectionRouter, [AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter, [AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter, [AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.BitBucket]: registerBitBucketConnectionRouter [AppConnection.Bitbucket]: registerBitbucketConnectionRouter
}; };

View File

@@ -28,7 +28,7 @@ export enum AppConnection {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
BitBucket = "bitbucket" Bitbucket = "bitbucket"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@@ -51,9 +51,9 @@ import {
validateAzureKeyVaultConnectionCredentials validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault"; } from "./azure-key-vault";
import { import {
BitBucketConnectionMethod, BitbucketConnectionMethod,
getBitBucketConnectionListItem, getBitbucketConnectionListItem,
validateBitBucketConnectionCredentials validateBitbucketConnectionCredentials
} from "./bitbucket"; } from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum"; import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
@@ -142,7 +142,7 @@ export const listAppConnectionOptions = () => {
getFlyioConnectionListItem(), getFlyioConnectionListItem(),
getGitLabConnectionListItem(), getGitLabConnectionListItem(),
getCloudflareConnectionListItem(), getCloudflareConnectionListItem(),
getBitBucketConnectionListItem() getBitbucketConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -223,7 +223,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials 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); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -260,7 +260,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case VercelConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.APIToken: case CloudflareConnectionMethod.APIToken:
case BitBucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@@ -341,7 +341,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.BitBucket]: platformManagedCredentialsNotSupported [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (

View File

@@ -30,7 +30,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io", [AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab", [AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare", [AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.BitBucket]: "BitBucket" [AppConnection.Bitbucket]: "Bitbucket"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -63,5 +63,5 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular, [AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular, [AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular, [AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.BitBucket]: AppConnectionPlanType.Regular [AppConnection.Bitbucket]: AppConnectionPlanType.Regular
}; };

View File

@@ -45,7 +45,7 @@ import { azureClientSecretsConnectionService } from "./azure-client-secrets/azur
import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas"; import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas";
import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service"; import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateBitBucketConnectionCredentialsSchema } from "./bitbucket"; import { ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket";
import { bitBucketConnectionService } from "./bitbucket/bitbucket-connection-service"; import { bitBucketConnectionService } from "./bitbucket/bitbucket-connection-service";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda"; import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service"; import { camundaConnectionService } from "./camunda/camunda-connection-service";
@@ -122,7 +122,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema, [AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema, [AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema, [AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.BitBucket]: ValidateBitBucketConnectionCredentialsSchema [AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({

View File

@@ -57,10 +57,10 @@ import {
TValidateAzureKeyVaultConnectionCredentialsSchema TValidateAzureKeyVaultConnectionCredentialsSchema
} from "./azure-key-vault"; } from "./azure-key-vault";
import { import {
TBitBucketConnection, TBitbucketConnection,
TBitBucketConnectionConfig, TBitbucketConnectionConfig,
TBitBucketConnectionInput, TBitbucketConnectionInput,
TValidateBitBucketConnectionCredentialsSchema TValidateBitbucketConnectionCredentialsSchema
} from "./bitbucket"; } from "./bitbucket";
import { import {
TCamundaConnection, TCamundaConnection,
@@ -202,7 +202,7 @@ export type TAppConnection = { id: string } & (
| TFlyioConnection | TFlyioConnection
| TGitLabConnection | TGitLabConnection
| TCloudflareConnection | TCloudflareConnection
| TBitBucketConnection | TBitbucketConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -239,7 +239,7 @@ export type TAppConnectionInput = { id: string } & (
| TFlyioConnectionInput | TFlyioConnectionInput
| TGitLabConnectionInput | TGitLabConnectionInput
| TCloudflareConnectionInput | TCloudflareConnectionInput
| TBitBucketConnectionInput | TBitbucketConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@@ -284,7 +284,7 @@ export type TAppConnectionConfig =
| TFlyioConnectionConfig | TFlyioConnectionConfig
| TGitLabConnectionConfig | TGitLabConnectionConfig
| TCloudflareConnectionConfig | TCloudflareConnectionConfig
| TBitBucketConnectionConfig; | TBitbucketConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -316,7 +316,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateFlyioConnectionCredentialsSchema | TValidateFlyioConnectionCredentialsSchema
| TValidateGitLabConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema
| TValidateBitBucketConnectionCredentialsSchema; | TValidateBitbucketConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@@ -1,3 +1,3 @@
export enum BitBucketConnectionMethod { export enum BitbucketConnectionMethod {
ApiToken = "api-token" ApiToken = "api-token"
} }

View File

@@ -5,18 +5,23 @@ import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
import { TBitBucketConnection, TBitBucketConnectionConfig, TBitBucketRepo } from "./bitbucket-connection-types"; import {
TBitbucketConnection,
TBitbucketConnectionConfig,
TBitbucketRepo,
TBitbucketWorkspace
} from "./bitbucket-connection-types";
export const getBitBucketConnectionListItem = () => { export const getBitbucketConnectionListItem = () => {
return { return {
name: "BitBucket" as const, name: "Bitbucket" as const,
app: AppConnection.BitBucket as const, app: AppConnection.Bitbucket as const,
methods: Object.values(BitBucketConnectionMethod) as [BitBucketConnectionMethod.ApiToken] 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 { try {
const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, { const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, {
headers: { headers: {
@@ -38,12 +43,17 @@ export const getBitBucketUser = async ({ email, apiToken }: { email: string; api
} }
}; };
export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => { export const validateBitbucketConnectionCredentials = async (config: TBitbucketConnectionConfig) => {
await getBitBucketUser(config.credentials); await getBitbucketUser(config.credentials);
return 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 { email, apiToken } = appConnection.credentials;
const headers = { const headers = {
@@ -51,19 +61,52 @@ export const listBitBucketRepositories = async (appConnection: TBitBucketConnect
Accept: "application/json" Accept: "application/json"
}; };
let allRepos: TBitBucketRepo[] = []; let allWorkspaces: TBitbucketWorkspace[] = [];
let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`; 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<BitbucketWorkspacesResponse>(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; let iterationCount = 0;
// Limit to 10 iterations, fetching at most 10 * 100 = 1000 repositories // Limit to 10 iterations, fetching at most 10 * 100 = 1000 repositories
while (nextUrl && iterationCount < 10) { while (nextUrl && iterationCount < 10) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const { data }: { data: { values: TBitBucketRepo[]; next?: string } } = await request.get<{ const { data }: { data: BitbucketRepositoriesResponse } = await request.get<BitbucketRepositoriesResponse>(
values: TBitBucketRepo[]; nextUrl,
next?: string; {
}>(nextUrl, { headers
headers }
}); );
allRepos = allRepos.concat(data.values); allRepos = allRepos.concat(data.values);
nextUrl = data.next; nextUrl = data.next;

View File

@@ -8,54 +8,54 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } 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), 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) 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({ export const BitbucketConnectionSchema = BaseBitbucketConnectionSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken), method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: BitBucketConnectionAccessTokenCredentialsSchema credentials: BitbucketConnectionAccessTokenCredentialsSchema
}); });
export const SanitizedBitBucketConnectionSchema = z.discriminatedUnion("method", [ export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method", [
BaseBitBucketConnectionSchema.extend({ BaseBitbucketConnectionSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken), method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: BitBucketConnectionAccessTokenCredentialsSchema.pick({ credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({
email: true email: true
}) })
}) })
]); ]);
export const ValidateBitBucketConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({ z.object({
method: z method: z
.literal(BitBucketConnectionMethod.ApiToken) .literal(BitbucketConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.BitBucket).method), .describe(AppConnections.CREATE(AppConnection.Bitbucket).method),
credentials: BitBucketConnectionAccessTokenCredentialsSchema.describe( credentials: BitbucketConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.BitBucket).credentials AppConnections.CREATE(AppConnection.Bitbucket).credentials
) )
}) })
]); ]);
export const CreateBitBucketConnectionSchema = ValidateBitBucketConnectionCredentialsSchema.and( export const CreateBitbucketConnectionSchema = ValidateBitbucketConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.BitBucket) GenericCreateAppConnectionFieldsSchema(AppConnection.Bitbucket)
); );
export const UpdateBitBucketConnectionSchema = z export const UpdateBitbucketConnectionSchema = z
.object({ .object({
credentials: BitBucketConnectionAccessTokenCredentialsSchema.optional().describe( credentials: BitbucketConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.BitBucket).credentials AppConnections.UPDATE(AppConnection.Bitbucket).credentials
) )
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.BitBucket)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket));
export const BitBucketConnectionListItemSchema = z.object({ export const BitbucketConnectionListItemSchema = z.object({
name: z.literal("BitBucket"), name: z.literal("Bitbucket"),
app: z.literal(AppConnection.BitBucket), app: z.literal(AppConnection.Bitbucket),
methods: z.nativeEnum(BitBucketConnectionMethod).array() methods: z.nativeEnum(BitbucketConnectionMethod).array()
}); });

View File

@@ -1,25 +1,33 @@
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { listBitBucketRepositories } from "./bitbucket-connection-fns"; import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns";
import { TBitBucketConnection } from "./bitbucket-connection-types"; import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types";
type TGetAppConnectionFunc = ( type TGetAppConnectionFunc = (
app: AppConnection, app: AppConnection,
connectionId: string, connectionId: string,
actor: OrgServiceActor actor: OrgServiceActor
) => Promise<TBitBucketConnection>; ) => Promise<TBitbucketConnection>;
export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => { export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.BitBucket, connectionId, actor); const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor);
const workspaces = await listBitbucketWorkspaces(appConnection);
return workspaces;
};
const repositories = await listBitBucketRepositories(appConnection); const listRepositories = async (
{ connectionId, workspaceSlug }: TGetBitbucketRepositoriesDTO,
return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name })); actor: OrgServiceActor
) => {
const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor);
const repositories = await listBitbucketRepositories(appConnection, workspaceSlug);
return repositories;
}; };
return { return {
listWorkspaces,
listRepositories listRepositories
}; };
}; };

View File

@@ -4,27 +4,36 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { import {
BitBucketConnectionSchema, BitbucketConnectionSchema,
CreateBitBucketConnectionSchema, CreateBitbucketConnectionSchema,
ValidateBitBucketConnectionCredentialsSchema ValidateBitbucketConnectionCredentialsSchema
} from "./bitbucket-connection-schemas"; } from "./bitbucket-connection-schemas";
export type TBitBucketConnection = z.infer<typeof BitBucketConnectionSchema>; export type TBitbucketConnection = z.infer<typeof BitbucketConnectionSchema>;
export type TBitBucketConnectionInput = z.infer<typeof CreateBitBucketConnectionSchema> & { export type TBitbucketConnectionInput = z.infer<typeof CreateBitbucketConnectionSchema> & {
app: AppConnection.BitBucket; app: AppConnection.Bitbucket;
}; };
export type TValidateBitBucketConnectionCredentialsSchema = typeof ValidateBitBucketConnectionCredentialsSchema; export type TValidateBitbucketConnectionCredentialsSchema = typeof ValidateBitbucketConnectionCredentialsSchema;
export type TBitBucketConnectionConfig = DiscriminativePick< export type TBitbucketConnectionConfig = DiscriminativePick<
TBitBucketConnectionInput, TBitbucketConnectionInput,
"method" | "app" | "credentials" "method" | "app" | "credentials"
> & { > & {
orgId: string; 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 full_name: string; // workspace-slug/repo-slug
slug: string; slug: string;
}; };

View File

@@ -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 { interface RepositoriesResponse {
size: number; size: number;
page: number; page: number;
@@ -1302,7 +1302,7 @@ export const getApps = async ({
}); });
case Integrations.BITBUCKET: case Integrations.BITBUCKET:
return getAppsBitBucket({ return getAppsBitbucket({
accessToken, accessToken,
workspaceSlug workspaceSlug
}); });

View File

@@ -342,7 +342,7 @@ export const getIntegrationOptions = async () => {
{ {
name: "Bitbucket", name: "Bitbucket",
slug: "bitbucket", slug: "bitbucket",
image: "BitBucket.png", image: "Bitbucket.png",
isAvailable: true, isAvailable: true,
type: "oauth", type: "oauth",
clientId: appCfg.CLIENT_ID_BITBUCKET, clientId: appCfg.CLIENT_ID_BITBUCKET,

View File

@@ -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, integration,
secrets, secrets,
accessToken accessToken
@@ -4832,7 +4832,7 @@ export const syncIntegrationSecrets = async ({
}); });
break; break;
case Integrations.BITBUCKET: case Integrations.BITBUCKET:
await syncSecretsBitBucket({ await syncSecretsBitbucket({
integration, integration,
secrets, secrets,
accessToken accessToken

View File

@@ -64,7 +64,7 @@ type ExchangeCodeGitlabResponse = {
created_at: number; created_at: number;
}; };
type ExchangeCodeBitBucketResponse = { type ExchangeCodeBitbucketResponse = {
access_token: string; access_token: string;
token_type: string; token_type: string;
expires_in: number; 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 * code-token exchange
*/ */
const exchangeCodeBitBucket = async ({ code }: { code: string }) => { const exchangeCodeBitbucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) { if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
@@ -403,7 +403,7 @@ const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
} }
const res = ( const res = (
await request.post<ExchangeCodeBitBucketResponse>( await request.post<ExchangeCodeBitbucketResponse>(
IntegrationUrls.BITBUCKET_TOKEN_URL, IntegrationUrls.BITBUCKET_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: "authorization_code", grant_type: "authorization_code",
@@ -490,7 +490,7 @@ export const exchangeCode = async ({
url url
}); });
case Integrations.BITBUCKET: case Integrations.BITBUCKET:
return exchangeCodeBitBucket({ return exchangeCodeBitbucket({
code code
}); });
default: default:
@@ -524,7 +524,7 @@ type RefreshTokenGitLabResponse = {
created_at: number; created_at: number;
}; };
type RefreshTokenBitBucketResponse = { type RefreshTokenBitbucketResponse = {
access_token: string; access_token: string;
token_type: string; token_type: string;
expires_in: number; 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 * 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 accessExpiresAt = new Date();
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) { if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
@@ -664,7 +664,7 @@ const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string
const { const {
data data
}: { }: {
data: RefreshTokenBitBucketResponse; data: RefreshTokenBitbucketResponse;
} = await request.post( } = await request.post(
IntegrationUrls.BITBUCKET_TOKEN_URL, IntegrationUrls.BITBUCKET_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
@@ -794,7 +794,7 @@ export const exchangeRefresh = async (
url url
}); });
case Integrations.BITBUCKET: case Integrations.BITBUCKET:
return exchangeRefreshBitBucket({ return exchangeRefreshBitbucket({
refreshToken refreshToken
}); });
case Integrations.GCP_SECRET_MANAGER: case Integrations.GCP_SECRET_MANAGER:

View File

@@ -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 | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |
| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | | [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available |
| [Windmill](/integrations/cloud/windmill) | 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 | | [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | | [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | | [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |

View File

@@ -669,11 +669,11 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
<Accordion title="Bitbucket"> <Accordion title="Bitbucket">
<ParamField query="CLIENT_ID_BITBUCKET" type="string" default="none" optional> <ParamField query="CLIENT_ID_BITBUCKET" type="string" default="none" optional>
OAuth2 client ID for BitBucket integration OAuth2 client ID for Bitbucket integration
</ParamField> </ParamField>
<ParamField query="CLIENT_SECRET_BITBUCKET" type="string" default="none" optional> <ParamField query="CLIENT_SECRET_BITBUCKET" type="string" default="none" optional>
OAuth2 client secret for BitBucket integration OAuth2 client secret for Bitbucket integration
</ParamField> </ParamField>
</Accordion> </Accordion>

View File

@@ -1,13 +1,15 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form"; 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 { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { import {
TBitBucketRepo, TBitbucketRepo,
useBitBucketConnectionListRepositories TBitbucketWorkspace,
useBitbucketConnectionListRepositories,
useBitbucketConnectionListWorkspaces
} from "@app/hooks/api/appConnections/bitbucket"; } from "@app/hooks/api/appConnections/bitbucket";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
@@ -19,18 +21,25 @@ enum ScanMethod {
SelectRepositories = "select-repositories" SelectRepositories = "select-repositories"
} }
export const BitBucketDataSourceConfigFields = () => { export const BitbucketDataSourceConfigFields = () => {
const { control, watch, setValue } = useFormContext< const { control, watch, setValue } = useFormContext<
TSecretScanningDataSourceForm & { TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.BitBucket; type: SecretScanningDataSource.Bitbucket;
} }
>(); >();
const connectionId = useWatch({ control, name: "connection.id" }); const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("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 } = 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"); const includeRepos = watch("config.includeRepos");
@@ -51,10 +60,50 @@ export const BitBucketDataSourceConfigFields = () => {
isUpdate={isUpdate} isUpdate={isUpdate}
onChange={() => { onChange={() => {
if (scanMethod === ScanMethod.SelectRepositories) { if (scanMethod === ScanMethod.SelectRepositories) {
setValue("config.workspaceSlug", "");
setValue("config.includeRepos", []); setValue("config.includeRepos", []);
} }
}} }}
/> />
<Controller
name="config.workspaceSlug"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Workspace"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the workspaces you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={areWorkspacesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={value ? { slug: value } : null}
onChange={(newValue) => {
onChange((newValue as SingleValue<TBitbucketWorkspace>)?.slug);
if (scanMethod === ScanMethod.SelectRepositories) {
setValue("config.includeRepos", []);
}
}}
options={workspaces}
placeholder="Select workspace..."
getOptionLabel={(option) => option.slug}
getOptionValue={(option) => option.slug}
/>
</FormControl>
)}
/>
<FormControl label="Scan Repositories"> <FormControl label="Scan Repositories">
<Select <Select
value={scanMethod} value={scanMethod}
@@ -78,7 +127,7 @@ export const BitBucketDataSourceConfigFields = () => {
{scanMethod === ScanMethod.SelectRepositories && ( {scanMethod === ScanMethod.SelectRepositories && (
<Controller <Controller
name="config.includeRepos" name="config.includeRepos"
defaultValue={["*"]} // defaultValue={["*"]} // This defaultValue is handled by the useEffect above
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
@@ -100,18 +149,22 @@ export const BitBucketDataSourceConfigFields = () => {
<FilterableSelect <FilterableSelect
menuPlacement="top" menuPlacement="top"
isLoading={areRepositoriesLoading && Boolean(connectionId)} isLoading={areRepositoriesLoading && Boolean(connectionId)}
isDisabled={!connectionId} // Disable if no connection or no workspace slug is selected for fetching repositories
isDisabled={!connectionId || !selectedWorkspaceSlug}
isMulti isMulti
value={repositories?.filter((repository) => value.includes(repository.name))} // Ensure 'value' is treated as a string array for filtering
value={repositories?.filter((repository) =>
(value as string[]).includes(repository.full_name)
)}
onChange={(newValue) => { onChange={(newValue) => {
onChange( onChange(
newValue ? (newValue as MultiValue<TBitBucketRepo>).map((p) => p.name) : null newValue ? (newValue as MultiValue<TBitbucketRepo>).map((p) => p.full_name) : [] // Ensure it's an empty array if cleared
); );
}} }}
options={repositories} options={repositories}
placeholder="Select repositories..." placeholder="Select repositories..."
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.full_name}
getOptionValue={(option) => option.name} getOptionValue={(option) => option.full_name}
/> />
</FormControl> </FormControl>
)} )}

View File

@@ -5,12 +5,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas"; import { TSecretScanningDataSourceForm } from "../schemas";
import { BitBucketDataSourceConfigFields } from "./BitBucketDataSourceConfigFields"; import { BitbucketDataSourceConfigFields } from "./BitbucketDataSourceConfigFields";
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields"; import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = { const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields, [SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
[SecretScanningDataSource.BitBucket]: BitBucketDataSourceConfigFields [SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields
}; };
export const SecretScanningDataSourceConfigFields = () => { export const SecretScanningDataSourceConfigFields = () => {

View File

@@ -6,10 +6,10 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas"; import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConfigReviewSection } from "./shared"; import { SecretScanningDataSourceConfigReviewSection } from "./shared";
export const BitBucketDataSourceReviewFields = () => { export const BitbucketDataSourceReviewFields = () => {
const { watch } = useFormContext< const { watch } = useFormContext<
TSecretScanningDataSourceForm & { TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.BitBucket; type: SecretScanningDataSource.Bitbucket;
} }
>(); >();

View File

@@ -4,12 +4,12 @@ import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas"; import { TSecretScanningDataSourceForm } from "../schemas";
import { BitBucketDataSourceReviewFields } from "./BitBucketDataSourceReviewFields"; import { BitbucketDataSourceReviewFields } from "./BitbucketDataSourceReviewFields";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields"; import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = { const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields, [SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
[SecretScanningDataSource.BitBucket]: BitBucketDataSourceReviewFields [SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields
}; };
export const SecretScanningDataSourceReviewFields = () => { export const SecretScanningDataSourceReviewFields = () => {

View File

@@ -4,10 +4,11 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema"; import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
export const BitBucketDataSourceSchema = z export const BitbucketDataSourceSchema = z
.object({ .object({
type: z.literal(SecretScanningDataSource.BitBucket), type: z.literal(SecretScanningDataSource.Bitbucket),
config: z.object({ config: z.object({
workspaceSlug: z.string().min(1, "Workspace Required").max(128),
includeRepos: z includeRepos: z
.string() .string()
.array() .array()

View File

@@ -1,11 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import { BitBucketDataSourceSchema } from "./bitbucket-data-source-schema"; import { BitbucketDataSourceSchema } from "./bitbucket-data-source-schema";
import { GitHubDataSourceSchema } from "./github-data-source-schema"; import { GitHubDataSourceSchema } from "./github-data-source-schema";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [ export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema, GitHubDataSourceSchema,
BitBucketDataSourceSchema BitbucketDataSourceSchema
]); ]);
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>; export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;

View File

@@ -39,7 +39,7 @@ import {
VercelConnectionMethod, VercelConnectionMethod,
WindmillConnectionMethod WindmillConnectionMethod
} from "@app/hooks/api/appConnections/types"; } from "@app/hooks/api/appConnections/types";
import { BitBucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
@@ -90,7 +90,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.BitBucket]: { name: "BitBucket", image: "BitBucket.png" } [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }
}; };
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -122,7 +122,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case VercelConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.ApiToken: case CloudflareConnectionMethod.ApiToken:
case BitBucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey }; return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:

View File

@@ -20,9 +20,9 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
image: "GitHub.png", image: "GitHub.png",
size: 45 size: 45
}, },
[SecretScanningDataSource.BitBucket]: { [SecretScanningDataSource.Bitbucket]: {
name: "BitBucket", name: "Bitbucket",
image: "BitBucket.png", image: "Bitbucket.png",
size: 45 size: 45
} }
}; };
@@ -32,7 +32,7 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
AppConnection AppConnection
> = { > = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar, [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.BitBucket]: AppConnection.BitBucket [SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
}; };
export const RESOURCE_DESCRIPTION_HELPER: Record< export const RESOURCE_DESCRIPTION_HELPER: Record<
@@ -52,7 +52,7 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
pluralTitle: "Repositories", pluralTitle: "Repositories",
singularTitle: "Repository" singularTitle: "Repository"
}, },
[SecretScanningDataSource.BitBucket]: { [SecretScanningDataSource.Bitbucket]: {
verb: "push", verb: "push",
pluralNoun: "repositories", pluralNoun: "repositories",
singularNoun: "repository", singularNoun: "repository",

View File

@@ -3,31 +3,67 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries"; import { appConnectionKeys } from "../queries";
import { TBitBucketConnectionListRepositoriesResponse, TBitBucketRepo } from "./types"; import {
TBitbucketConnectionListRepositoriesResponse,
TBitbucketConnectionListWorkspacesResponse,
TBitbucketRepo,
TBitbucketWorkspace
} from "./types";
const bitBucketConnectionKeys = { const bitBucketConnectionKeys = {
all: [...appConnectionKeys.all, "bitbucket"] as const, all: [...appConnectionKeys.all, "bitbucket"] as const,
listRepos: (connectionId: string) => listRepos: (connectionId: string, workspaceSlug: string) =>
[...bitBucketConnectionKeys.all, "repos", connectionId] as const [...bitBucketConnectionKeys.all, "repos", connectionId, workspaceSlug] as const,
listWorkspaces: (connectionId: string) =>
[...bitBucketConnectionKeys.all, "workspaces", connectionId] as const
}; };
export const useBitBucketConnectionListRepositories = ( export const useBitbucketConnectionListWorkspaces = (
connectionId: string, connectionId: string,
options?: Omit< options?: Omit<
UseQueryOptions< UseQueryOptions<
TBitBucketRepo[], TBitbucketWorkspace[],
unknown, unknown,
TBitBucketRepo[], TBitbucketWorkspace[],
ReturnType<typeof bitBucketConnectionKeys.listWorkspaces>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: bitBucketConnectionKeys.listWorkspaces(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TBitbucketConnectionListWorkspacesResponse>(
`/api/v1/app-connections/bitbucket/${connectionId}/workspaces`
);
return data.workspaces;
},
...options
});
};
export const useBitbucketConnectionListRepositories = (
connectionId: string,
workspaceSlug: string,
options?: Omit<
UseQueryOptions<
TBitbucketRepo[],
unknown,
TBitbucketRepo[],
// The ReturnType here will be a supertype of the actual queryKey, which is acceptable
ReturnType<typeof bitBucketConnectionKeys.listRepos> ReturnType<typeof bitBucketConnectionKeys.listRepos>
>, >,
"queryKey" | "queryFn" "queryKey" | "queryFn"
> >
) => { ) => {
return useQuery({ return useQuery({
queryKey: bitBucketConnectionKeys.listRepos(connectionId), // Append workspaceSlug to the existing query key for unique caching
queryKey: bitBucketConnectionKeys.listRepos(connectionId, workspaceSlug),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<TBitBucketConnectionListRepositoriesResponse>( const { data } = await apiRequest.get<TBitbucketConnectionListRepositoriesResponse>(
`/api/v1/app-connections/bitbucket/${connectionId}/repositories` // Include workspaceSlug in the API endpoint path
`/api/v1/app-connections/bitbucket/${connectionId}/repositories?workspaceSlug=${workspaceSlug}`
); );
return data.repositories; return data.repositories;

View File

@@ -1,8 +1,16 @@
export type TBitBucketRepo = { export type TBitbucketWorkspace = {
id: string; slug: string;
name: string; // workspace-slug/repo-slug
}; };
export type TBitBucketConnectionListRepositoriesResponse = { export type TBitbucketRepo = {
repositories: TBitBucketRepo[]; slug: string;
full_name: string; // workspace-slug/repo-slug
};
export type TBitbucketConnectionListWorkspacesResponse = {
workspaces: TBitbucketWorkspace[];
};
export type TBitbucketConnectionListRepositoriesResponse = {
repositories: TBitbucketRepo[];
}; };

View File

@@ -28,5 +28,5 @@ export enum AppConnection {
Flyio = "flyio", Flyio = "flyio",
Gitlab = "gitlab", Gitlab = "gitlab",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
BitBucket = "bitbucket" Bitbucket = "bitbucket"
} }

View File

@@ -132,8 +132,8 @@ export type TCloudflareConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Cloudflare; app: AppConnection.Cloudflare;
}; };
export type TBitBucketConnectionOption = TAppConnectionOptionBase & { export type TBitbucketConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.BitBucket; app: AppConnection.Bitbucket;
}; };
export type TAppConnectionOption = export type TAppConnectionOption =
@@ -164,7 +164,7 @@ export type TAppConnectionOption =
| TFlyioConnectionOption | TFlyioConnectionOption
| TGitlabConnectionOption | TGitlabConnectionOption
| TCloudflareConnectionOption | TCloudflareConnectionOption
| TBitBucketConnectionOption; | TBitbucketConnectionOption;
export type TAppConnectionOptionMap = { export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption; [AppConnection.AWS]: TAwsConnectionOption;
@@ -196,5 +196,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Flyio]: TFlyioConnectionOption; [AppConnection.Flyio]: TFlyioConnectionOption;
[AppConnection.Gitlab]: TGitlabConnectionOption; [AppConnection.Gitlab]: TGitlabConnectionOption;
[AppConnection.Cloudflare]: TCloudflareConnectionOption; [AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.BitBucket]: TBitBucketConnectionOption; [AppConnection.Bitbucket]: TBitbucketConnectionOption;
}; };

View File

@@ -1,12 +1,12 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum BitBucketConnectionMethod { export enum BitbucketConnectionMethod {
ApiToken = "api-token" ApiToken = "api-token"
} }
export type TBitBucketConnection = TRootAppConnection & { app: AppConnection.BitBucket } & { export type TBitbucketConnection = TRootAppConnection & { app: AppConnection.Bitbucket } & {
method: BitBucketConnectionMethod.ApiToken; method: BitbucketConnectionMethod.ApiToken;
credentials: { credentials: {
email: string; email: string;
apiToken: string; apiToken: string;

View File

@@ -7,7 +7,7 @@ import { TAzureAppConfigurationConnection } from "./azure-app-configuration-conn
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection"; import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
import { TAzureDevOpsConnection } from "./azure-devops-connection"; import { TAzureDevOpsConnection } from "./azure-devops-connection";
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
import { TBitBucketConnection } from "./bitbucket-connection"; import { TBitbucketConnection } from "./bitbucket-connection";
import { TCamundaConnection } from "./camunda-connection"; import { TCamundaConnection } from "./camunda-connection";
import { TCloudflareConnection } from "./cloudflare-connection"; import { TCloudflareConnection } from "./cloudflare-connection";
import { TDatabricksConnection } from "./databricks-connection"; import { TDatabricksConnection } from "./databricks-connection";
@@ -92,7 +92,7 @@ export type TAppConnection =
| TFlyioConnection | TFlyioConnection
| TGitLabConnection | TGitLabConnection
| TCloudflareConnection | TCloudflareConnection
| TBitBucketConnection; | TBitbucketConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">; export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -149,5 +149,5 @@ export type TAppConnectionMap = {
[AppConnection.Flyio]: TFlyioConnection; [AppConnection.Flyio]: TFlyioConnection;
[AppConnection.Gitlab]: TGitLabConnection; [AppConnection.Gitlab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection; [AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.BitBucket]: TBitBucketConnection; [AppConnection.Bitbucket]: TBitbucketConnection;
}; };

View File

@@ -4,7 +4,7 @@ export {
useDeleteIntegrationAuth, useDeleteIntegrationAuth,
useDeleteIntegrationAuths, useDeleteIntegrationAuths,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthBitbucketWorkspaces,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthChecklyGroups,
useGetIntegrationAuthCircleCIOrganizations, useGetIntegrationAuthCircleCIOrganizations,

View File

@@ -6,8 +6,8 @@ import { TReactQueryOptions } from "@app/types/reactQuery";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { import {
App, App,
BitBucketEnvironment, BitbucketEnvironment,
BitBucketWorkspace, BitbucketWorkspace,
ChecklyGroup, ChecklyGroup,
CircleCIOrganization, CircleCIOrganization,
Environment, Environment,
@@ -98,9 +98,9 @@ const integrationAuthKeys = {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const, }) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) => getIntegrationAuthBitbucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const, [{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
getIntegrationAuthBitBucketEnvironments: ( getIntegrationAuthBitbucketEnvironments: (
integrationAuthId: string, integrationAuthId: string,
workspaceSlug: string, workspaceSlug: string,
repoSlug: string repoSlug: string
@@ -446,23 +446,23 @@ const fetchIntegrationAuthRailwayServices = async ({
return services; return services;
}; };
const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => { const fetchIntegrationAuthBitbucketWorkspaces = async (integrationAuthId: string) => {
const { const {
data: { workspaces } data: { workspaces }
} = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>( } = await apiRequest.get<{ workspaces: BitbucketWorkspace[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces` `/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces`
); );
return workspaces; return workspaces;
}; };
const fetchIntegrationAuthBitBucketEnvironments = async ( const fetchIntegrationAuthBitbucketEnvironments = async (
integrationAuthId: string, integrationAuthId: string,
workspaceSlug: string, workspaceSlug: string,
repoSlug: string repoSlug: string
) => { ) => {
const { const {
data: { environments } data: { environments }
} = await apiRequest.get<{ environments: BitBucketEnvironment[] }>( } = await apiRequest.get<{ environments: BitbucketEnvironment[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/environments`, `/api/v1/integration-auth/${integrationAuthId}/bitbucket/environments`,
{ {
params: { params: {
@@ -833,10 +833,10 @@ export const useGetIntegrationAuthRailwayServices = ({
}); });
}; };
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => { export const useGetIntegrationAuthBitbucketWorkspaces = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthBitbucketWorkspaces(integrationAuthId),
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId), queryFn: () => fetchIntegrationAuthBitbucketWorkspaces(integrationAuthId),
enabled: true enabled: true
}); });
}; };
@@ -858,7 +858,7 @@ export const useGetIntegrationAuthOctopusDeployScopeValues = (
...options ...options
}); });
export const useGetIntegrationAuthBitBucketEnvironments = ( export const useGetIntegrationAuthBitbucketEnvironments = (
{ {
integrationAuthId, integrationAuthId,
workspaceSlug, workspaceSlug,
@@ -871,13 +871,13 @@ export const useGetIntegrationAuthBitBucketEnvironments = (
options?: TReactQueryOptions["options"] options?: TReactQueryOptions["options"]
) => { ) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments( queryKey: integrationAuthKeys.getIntegrationAuthBitbucketEnvironments(
integrationAuthId, integrationAuthId,
workspaceSlug, workspaceSlug,
repoSlug repoSlug
), ),
queryFn: () => queryFn: () =>
fetchIntegrationAuthBitBucketEnvironments(integrationAuthId, workspaceSlug, repoSlug), fetchIntegrationAuthBitbucketEnvironments(integrationAuthId, workspaceSlug, repoSlug),
...options ...options
}); });
}; };

View File

@@ -78,13 +78,13 @@ export type Service = {
serviceId: string; serviceId: string;
}; };
export type BitBucketWorkspace = { export type BitbucketWorkspace = {
uuid: string; uuid: string;
name: string; name: string;
slug: string; slug: string;
}; };
export type BitBucketEnvironment = { export type BitbucketEnvironment = {
uuid: string; uuid: string;
name: string; name: string;
slug: string; slug: string;

View File

@@ -1,6 +1,6 @@
export enum SecretScanningDataSource { export enum SecretScanningDataSource {
GitHub = "github", GitHub = "github",
BitBucket = "bitbucket" Bitbucket = "bitbucket"
} }
export enum SecretScanningScanStatus { export enum SecretScanningScanStatus {

View File

@@ -3,15 +3,15 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretScanningDataSource } from "../enums"; import { SecretScanningDataSource } from "../enums";
import { TSecretScanningDataSourceBase } from "./shared"; import { TSecretScanningDataSourceBase } from "./shared";
export type TBitBucketDataSource = TSecretScanningDataSourceBase & { export type TBitbucketDataSource = TSecretScanningDataSourceBase & {
type: SecretScanningDataSource.BitBucket; type: SecretScanningDataSource.Bitbucket;
config: { config: {
includeRepos: string[]; includeRepos: string[];
}; };
}; };
export type TBitBucketDataSourceOption = { export type TBitbucketDataSourceOption = {
name: string; name: string;
type: SecretScanningDataSource.BitBucket; type: SecretScanningDataSource.Bitbucket;
connection: AppConnection.BitBucket; connection: AppConnection.Bitbucket;
}; };

View File

@@ -8,10 +8,10 @@ import {
SecretScanningScanStatus, SecretScanningScanStatus,
SecretScanningScanType SecretScanningScanType
} from "../enums"; } from "../enums";
import { TBitBucketDataSource, TBitBucketDataSourceOption } from "./bitbucket-data-source"; import { TBitbucketDataSource, TBitbucketDataSourceOption } from "./bitbucket-data-source";
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source"; import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource; export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & { export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt: string | null; lastScannedAt: string | null;
@@ -24,7 +24,7 @@ export type TListSecretScanningDataSources = {
dataSources: TSecretScanningDataSourceWithDetails[]; dataSources: TSecretScanningDataSourceWithDetails[];
}; };
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitBucketDataSourceOption; export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitbucketDataSourceOption;
export type TListSecretScanningDataSourceOptions = { export type TListSecretScanningDataSourceOptions = {
dataSourceOptions: TSecretScanningDataSourceOption[]; dataSourceOptions: TSecretScanningDataSourceOption[];

View File

@@ -16,7 +16,7 @@ import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConn
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm"; import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm"; import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
import { BitBucketConnectionForm } from "./BitBucketConnectionForm"; import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
@@ -135,8 +135,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <GitLabConnectionForm onSubmit={onSubmit} />; return <GitLabConnectionForm onSubmit={onSubmit} />;
case AppConnection.Cloudflare: case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />; return <CloudflareConnectionForm onSubmit={onSubmit} />;
case AppConnection.BitBucket: case AppConnection.Bitbucket:
return <BitBucketConnectionForm onSubmit={onSubmit} />; return <BitbucketConnectionForm onSubmit={onSubmit} />;
default: default:
throw new Error(`Unhandled App ${app}`); throw new Error(`Unhandled App ${app}`);
} }
@@ -231,8 +231,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Cloudflare: case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.BitBucket: case AppConnection.Bitbucket:
return <BitBucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default: default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
} }

View File

@@ -12,7 +12,7 @@ import {
SelectItem SelectItem
} from "@app/components/v2"; } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { BitBucketConnectionMethod, TBitBucketConnection } from "@app/hooks/api/appConnections"; import { BitbucketConnectionMethod, TBitbucketConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { import {
@@ -21,17 +21,17 @@ import {
} from "./GenericAppConnectionFields"; } from "./GenericAppConnectionFields";
type Props = { type Props = {
appConnection?: TBitBucketConnection; appConnection?: TBitbucketConnection;
onSubmit: (formData: FormData) => void; onSubmit: (formData: FormData) => void;
}; };
const rootSchema = genericAppConnectionFieldsSchema.extend({ const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.BitBucket) app: z.literal(AppConnection.Bitbucket)
}); });
const formSchema = z.discriminatedUnion("method", [ const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({ rootSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken), method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: z.object({ credentials: z.object({
email: z.string().email().trim().min(1, "Email required"), email: z.string().email().trim().min(1, "Email required"),
apiToken: z.string().trim().min(1, "API Token required") apiToken: z.string().trim().min(1, "API Token required")
@@ -41,14 +41,14 @@ const formSchema = z.discriminatedUnion("method", [
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
export const BitBucketConnectionForm = ({ appConnection, onSubmit }: Props) => { export const BitbucketConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection); const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: appConnection ?? { defaultValues: appConnection ?? {
app: AppConnection.BitBucket, app: AppConnection.Bitbucket,
method: BitBucketConnectionMethod.ApiToken method: BitbucketConnectionMethod.ApiToken
} }
}); });
@@ -68,7 +68,7 @@ export const BitBucketConnectionForm = ({ appConnection, onSubmit }: Props) => {
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
tooltipText={`The method you would like to use to connect with ${ tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.BitBucket].name APP_CONNECTION_MAP[AppConnection.Bitbucket].name
}. This field cannot be changed after creation.`} }. This field cannot be changed after creation.`}
errorText={error?.message} errorText={error?.message}
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
@@ -82,7 +82,7 @@ export const BitBucketConnectionForm = ({ appConnection, onSubmit }: Props) => {
position="popper" position="popper"
dropdownContainerClassName="max-w-none" dropdownContainerClassName="max-w-none"
> >
{Object.values(BitBucketConnectionMethod).map((method) => { {Object.values(BitbucketConnectionMethod).map((method) => {
return ( return (
<SelectItem value={method} key={method}> <SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "} {getAppConnectionMethodDetails(method).name}{" "}
@@ -130,7 +130,7 @@ export const BitBucketConnectionForm = ({ appConnection, onSubmit }: Props) => {
isLoading={isSubmitting} isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty} isDisabled={isSubmitting || !isDirty}
> >
{isUpdate ? "Update Credentials" : "Connect to BitBucket"} {isUpdate ? "Update Credentials" : "Connect to Bitbucket"}
</Button> </Button>
<ModalClose asChild> <ModalClose asChild>
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">

View File

@@ -20,9 +20,9 @@ import { useWorkspace } from "@app/context";
import { import {
useCreateIntegration, useCreateIntegration,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces useGetIntegrationAuthBitbucketWorkspaces
} from "@app/hooks/api"; } from "@app/hooks/api";
import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries"; import { useGetIntegrationAuthBitbucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationsListPageTabs } from "@app/types/integrations"; import { IntegrationsListPageTabs } from "@app/types/integrations";
enum BitbucketScope { enum BitbucketScope {
@@ -99,7 +99,7 @@ export const BitbucketConfigurePage = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } = const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } =
useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? ""); useGetIntegrationAuthBitbucketWorkspaces((integrationAuthId as string) ?? "");
const { data: bitbucketRepos, isPending: isBitbucketReposLoading } = useGetIntegrationAuthApps({ const { data: bitbucketRepos, isPending: isBitbucketReposLoading } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "", integrationAuthId: (integrationAuthId as string) ?? "",
@@ -107,7 +107,7 @@ export const BitbucketConfigurePage = () => {
}); });
const { data: bitbucketEnvironments, isPending: isBitbucketEnvironmentsLoading } = const { data: bitbucketEnvironments, isPending: isBitbucketEnvironmentsLoading } =
useGetIntegrationAuthBitBucketEnvironments( useGetIntegrationAuthBitbucketEnvironments(
{ {
integrationAuthId: (integrationAuthId as string) ?? "", integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug ?? "", workspaceSlug: bitBucketWorkspace?.slug ?? "",

View File

@@ -1,11 +1,11 @@
import { GenericFieldLabel } from "@app/components/v2"; import { GenericFieldLabel } from "@app/components/v2";
import { TBitBucketDataSource } from "@app/hooks/api/secretScanningV2/types/bitbucket-data-source"; import { TBitbucketDataSource } from "@app/hooks/api/secretScanningV2/types/bitbucket-data-source";
type Props = { type Props = {
dataSource: TBitBucketDataSource; dataSource: TBitbucketDataSource;
}; };
export const BitBucketDataSourceConfigDisplay = ({ dataSource }: Props) => { export const BitbucketDataSourceConfigDisplay = ({ dataSource }: Props) => {
const { const {
config: { includeRepos } config: { includeRepos }
} = dataSource; } = dataSource;

View File

@@ -3,8 +3,8 @@ import {
TSecretScanningDataSource TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2"; } from "@app/hooks/api/secretScanningV2";
import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay";
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay"; import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
import { BitBucketDataSourceConfigDisplay } from "./BitBucketDataSourceConfigDisplay";
type Props = { type Props = {
dataSource: TSecretScanningDataSource; dataSource: TSecretScanningDataSource;
@@ -14,8 +14,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
switch (dataSource.type) { switch (dataSource.type) {
case SecretScanningDataSource.GitHub: case SecretScanningDataSource.GitHub:
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />; return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.BitBucket: case SecretScanningDataSource.Bitbucket:
return <BitBucketDataSourceConfigDisplay dataSource={dataSource} />; return <BitbucketDataSourceConfigDisplay dataSource={dataSource} />;
default: default:
throw new Error( throw new Error(
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}` `Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`