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
@@ -1,16 +1,16 @@
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
import {
BitBucketDataSourceSchema,
CreateBitBucketDataSourceSchema,
UpdateBitBucketDataSourceSchema
BitbucketDataSourceSchema,
CreateBitbucketDataSourceSchema,
UpdateBitbucketDataSourceSchema
} from "@app/ee/services/secret-scanning-v2/bitbucket";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export const registerBitBucketSecretScanningRouter = async (server: FastifyZodProvider) =>
export const registerBitbucketSecretScanningRouter = async (server: FastifyZodProvider) =>
registerSecretScanningEndpoints({
type: SecretScanningDataSource.BitBucket,
type: SecretScanningDataSource.Bitbucket,
server,
responseSchema: BitBucketDataSourceSchema,
createSchema: CreateBitBucketDataSourceSchema,
updateSchema: UpdateBitBucketDataSourceSchema
responseSchema: BitbucketDataSourceSchema,
createSchema: CreateBitbucketDataSourceSchema,
updateSchema: UpdateBitbucketDataSourceSchema
});
@@ -1,6 +1,6 @@
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { registerBitBucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router";
export * from "./secret-scanning-v2-router";
@@ -10,5 +10,5 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
(server: FastifyZodProvider) => Promise<void>
> = {
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
[SecretScanningDataSource.BitBucket]: registerBitBucketSecretScanningRouter
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter
};
@@ -2,7 +2,7 @@ import { z } from "zod";
import { SecretScanningConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { BitBucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
import {
SecretScanningFindingStatus,
@@ -24,7 +24,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
GitHubDataSourceListItemSchema,
BitBucketDataSourceListItemSchema
BitbucketDataSourceListItemSchema
]);
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {
+28 -28
View File
@@ -18,33 +18,33 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0,
identityLimit: null,
identitiesUsed: 0,
dynamicSecret: false,
dynamicSecret: true,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: false,
githubOrgSync: false,
pitRecovery: true,
ipAllowlisting: true,
rbac: true,
githubOrgSync: true,
customRateLimits: false,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,
auditLogsRetentionDays: 0,
auditLogStreams: false,
customAlerts: true,
secretAccessInsights: true,
auditLogs: true,
auditLogsRetentionDays: 3,
auditLogStreams: true,
auditLogStreamLimit: 3,
samlSSO: false,
hsm: false,
oidcSSO: false,
scim: false,
ldap: false,
groups: false,
samlSSO: true,
hsm: true,
oidcSSO: true,
scim: true,
ldap: true,
groups: true,
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretRotation: false,
caCrl: false,
instanceUserManagement: false,
externalKms: false,
secretApproval: true,
secretRotation: true,
caCrl: true,
instanceUserManagement: true,
externalKms: true,
rateLimits: {
readLimit: 60,
writeLimit: 200,
@@ -52,13 +52,13 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
},
pkiEst: false,
enforceMfa: false,
projectTemplates: false,
kmip: false,
gateway: false,
sshHostGroups: false,
secretScanning: false,
enterpriseSecretSyncs: false,
enterpriseAppConnections: false
projectTemplates: true,
kmip: true,
gateway: true,
sshHostGroups: true,
enterpriseAppConnections: true,
enterpriseSecretSyncs: true,
secretScanning: true
});
export const setupLicenseRequestWithStore = (
@@ -3,7 +3,7 @@ import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scann
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = {
name: "BitBucket",
type: SecretScanningDataSource.BitBucket,
connection: AppConnection.BitBucket
name: "Bitbucket",
type: SecretScanningDataSource.Bitbucket,
connection: AppConnection.Bitbucket
};
@@ -3,7 +3,6 @@ import { join } from "path";
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import {
SecretScanningDataSource,
SecretScanningFindingSeverity,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
@@ -20,54 +19,106 @@ import {
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { titleCaseToCamelCase } from "@app/lib/fn";
import { GitHubRepositoryRegex } from "@app/lib/regex";
import {
getBitBucketUser,
listBitBucketRepositories,
TBitBucketConnection
getBitbucketUser,
listBitbucketRepositories,
TBitbucketConnection
} from "@app/services/app-connection/bitbucket";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TBitBucketDataSourceWithConnection, TQueueBitBucketResourceDiffScan } from "./bitbucket-secret-scanning-types";
import { TBitbucketDataSourceWithConnection, TQueueBitbucketResourceDiffScan } from "./bitbucket-secret-scanning-types";
export const BitBucketSecretScanningFactory = () => {
const initialize: TSecretScanningFactoryInitialize<TBitBucketConnection> = async (
{ connection, secretScanningV2DAL },
export const BitbucketSecretScanningFactory = () => {
const initialize: TSecretScanningFactoryInitialize<TBitbucketConnection> = async (
{ connection, payload },
callback
) => {
// TODO(andrey): Swap for something proper
const externalId = connection.credentials.email;
const { email, apiToken } = connection.credentials;
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
const existingDataSource = await secretScanningV2DAL.dataSources.findOne({
externalId,
type: SecretScanningDataSource.BitBucket
});
if (existingDataSource)
throw new BadRequestError({
message: `A Data Source already exists for this BitBucket Radar Connection in the Project with ID "${existingDataSource.projectId}"`
});
const { data } = await request.post<{ uuid: string }>(
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks`,
{
description: "Infisical webhook for push events",
url: `https://tunnel.util.lol/secret-scanning/webhooks/bitbucket`, // TODO(andrey): Swap to ${cfg.SITE_URL}
active: true,
events: ["repo:push"]
},
{
headers: {
Authorization: authHeader,
Accept: "application/json"
}
}
);
return callback({
externalId
credentials: { webhookId: data.uuid }
});
};
const postInitialization: TSecretScanningFactoryPostInitialization<TBitBucketConnection> = async () => {
// no post-initialization required
const postInitialization: TSecretScanningFactoryPostInitialization<TBitbucketConnection> = async ({
dataSourceId,
credentials,
connection,
payload
}) => {
const { email, apiToken } = connection.credentials;
const { webhookId } = credentials;
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
const cfg = getConfig();
const newWebhookUrl = `${cfg.SITE_URL}/secret-scanning/webhooks/bitbucket?dataSourceId=${dataSourceId}`;
await request.put(
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks/${webhookId}`,
{
description: "Infisical webhook for push events",
url: newWebhookUrl,
active: true,
events: ["repo:push"]
},
{
headers: {
Authorization: authHeader,
Accept: "application/json"
}
}
);
};
const listRawResources: TSecretScanningFactoryListRawResources<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
) => {
const {
connection,
config: { includeRepos }
config: { includeRepos, workspaceSlug }
} = dataSource;
const repos = await listBitBucketRepositories(connection);
const repos = await listBitbucketRepositories(connection, workspaceSlug);
const filteredRepos: typeof repos = [];
if (includeRepos.includes("*")) {
@@ -83,7 +134,7 @@ export const BitBucketSecretScanningFactory = () => {
}));
};
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TBitBucketDataSourceWithConnection> = async ({
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TBitbucketDataSourceWithConnection> = async ({
dataSource,
resourceName,
tempFolder
@@ -97,10 +148,10 @@ export const BitBucketSecretScanningFactory = () => {
const repoPath = join(tempFolder, "repo.git");
if (!GitHubRepositoryRegex.test(resourceName)) {
throw new Error("Invalid BitBucket repository name");
throw new Error("Invalid Bitbucket repository name");
}
const { username } = await getBitBucketUser({ email, apiToken });
const { username } = await getBitbucketUser({ email, apiToken });
await cloneRepository({
cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`,
@@ -111,18 +162,18 @@ export const BitBucketSecretScanningFactory = () => {
};
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
TQueueBitBucketResourceDiffScan["payload"]
> = ({ repository }) => {
TQueueBitbucketResourceDiffScan["payload"]
> = ({ repository, dataSourceId }) => {
return {
name: repository.full_name,
externalId: repository.id.toString(),
externalId: dataSourceId,
type: SecretScanningResource.Repository
};
};
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
TBitBucketDataSourceWithConnection,
TQueueBitBucketResourceDiffScan["payload"]
TBitbucketDataSourceWithConnection,
TQueueBitbucketResourceDiffScan["payload"]
> = async ({ dataSource, payload, resourceName, configPath }) => {
const {
connection: {
@@ -130,81 +181,86 @@ export const BitBucketSecretScanningFactory = () => {
}
} = dataSource;
const { commits, repository } = payload;
const { push, repository } = payload;
const allFindings: SecretMatch[] = [];
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
for (const commit of commits) {
// eslint-disable-next-line no-await-in-loop
const { data: diffstat } = await request.get<{
values: {
status: "added" | "modified" | "removed" | "renamed";
new?: { path: string };
old?: { path: string };
}[];
}>(`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diffstat/${commit.id}`, {
headers: {
Authorization: authHeader,
Accept: "application/json"
}
});
for (const change of push.changes) {
for (const commit of change.commits) {
// eslint-disable-next-line no-await-in-loop
const { data: diffstat } = await request.get<{
values: {
status: "added" | "modified" | "removed" | "renamed";
new?: { path: string };
old?: { path: string };
}[];
}>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${repository.full_name}/diffstat/${commit.hash}`, {
headers: {
Authorization: authHeader,
Accept: "application/json"
}
});
// eslint-disable-next-line no-continue
if (!diffstat.values) continue;
// eslint-disable-next-line no-continue
if (!diffstat.values) continue;
for (const file of diffstat.values) {
if ((file.status === "added" || file.status === "modified") && file.new?.path) {
const filePath = file.new.path;
for (const file of diffstat.values) {
if ((file.status === "added" || file.status === "modified") && file.new?.path) {
const filePath = file.new.path;
// eslint-disable-next-line no-await-in-loop
const { data: patch } = await request.get<string>(
`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.id}`,
{
params: {
path: filePath
},
headers: {
Authorization: authHeader
},
responseType: "text"
}
);
// eslint-disable-next-line no-await-in-loop
const { data: patch } = await request.get<string>(
`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.hash}`,
{
params: {
path: filePath
},
headers: {
Authorization: authHeader
},
responseType: "text"
}
);
// eslint-disable-next-line no-continue
if (!patch) continue;
// eslint-disable-next-line no-continue
if (!patch) continue;
// eslint-disable-next-line
const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath);
// eslint-disable-next-line no-await-in-loop
const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath);
const adjustedFindings = findings.map((finding) => {
const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine);
const endLine =
finding.StartLine === finding.EndLine
? startLine
: convertPatchLineToFileLineNumber(patch, finding.EndLine);
const startColumn = finding.StartColumn - 1; // subtract 1 for +
const endColumn = finding.EndColumn - 1; // subtract 1 for +
const adjustedFindings = findings.map((finding) => {
const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine);
const endLine =
finding.StartLine === finding.EndLine
? startLine
: convertPatchLineToFileLineNumber(patch, finding.EndLine);
const startColumn = finding.StartColumn - 1; // subtract 1 for +
const endColumn = finding.EndColumn - 1; // subtract 1 for +
const authorName = commit.author.user?.display_name || commit.author.raw.split(" <")[0];
const emailMatch = commit.author.raw.match(/<(.*)>/);
const authorEmail = emailMatch?.[1] ?? "";
return {
...finding,
StartLine: startLine,
EndLine: endLine,
StartColumn: startColumn,
EndColumn: endColumn,
File: filePath,
Commit: commit.id,
Author: commit.author.name,
Email: commit.author.email ?? "",
Message: commit.message,
Fingerprint: `${commit.id}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`,
Date: commit.timestamp,
Link: `https://bitbucket.org/${resourceName}/src/${commit.id}/${filePath}#lines-${startLine}`
};
});
return {
...finding,
StartLine: startLine,
EndLine: endLine,
StartColumn: startColumn,
EndColumn: endColumn,
File: filePath,
Commit: commit.hash,
Author: authorName,
Email: authorEmail,
Message: commit.message,
Fingerprint: `${commit.hash}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`,
Date: commit.date,
Link: `https://bitbucket.org/${resourceName}/src/${commit.hash}/${filePath}#lines-${startLine}`
};
});
allFindings.push(...adjustedFindings);
allFindings.push(...adjustedFindings);
}
}
}
}
@@ -15,7 +15,12 @@ import { SecretScanningDataSources } from "@app/lib/api-docs";
import { GitHubRepositoryRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const BitBucketDataSourceConfigSchema = z.object({
export const BitbucketDataSourceConfigSchema = z.object({
workspaceSlug: z
.string()
.min(1, "Workspace slug required")
.max(128)
.describe(SecretScanningDataSources.CONFIG.BITBUCKET.workspaceSlug),
includeRepos: z
.array(
z
@@ -30,58 +35,62 @@ export const BitBucketDataSourceConfigSchema = z.object({
.describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos)
});
export const BitBucketDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.BitBucket,
export const BitbucketDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.Bitbucket,
isConnectionRequired: true
})
.extend({
config: BitBucketDataSourceConfigSchema
config: BitbucketDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "BitBucket"
title: "Bitbucket"
})
);
export const CreateBitBucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.BitBucket,
export const CreateBitbucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.Bitbucket,
isConnectionRequired: true
})
.extend({
config: BitBucketDataSourceConfigSchema
config: BitbucketDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "BitBucket"
title: "Bitbucket"
})
);
export const UpdateBitBucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(
SecretScanningDataSource.BitBucket
export const UpdateBitbucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(
SecretScanningDataSource.Bitbucket
)
.extend({
config: BitBucketDataSourceConfigSchema.optional()
config: BitbucketDataSourceConfigSchema.optional()
})
.describe(
JSON.stringify({
title: "BitBucket"
title: "Bitbucket"
})
);
export const BitBucketDataSourceListItemSchema = z
export const BitbucketDataSourceListItemSchema = z
.object({
name: z.literal("BitBucket"),
connection: z.literal(AppConnection.BitBucket),
type: z.literal(SecretScanningDataSource.BitBucket)
name: z.literal("Bitbucket"),
connection: z.literal(AppConnection.Bitbucket),
type: z.literal(SecretScanningDataSource.Bitbucket)
})
.describe(
JSON.stringify({
title: "BitBucket"
title: "Bitbucket"
})
);
export const BitBucketFindingSchema = BaseSecretScanningFindingSchema.extend({
export const BitbucketFindingSchema = BaseSecretScanningFindingSchema.extend({
resourceType: z.literal(SecretScanningResource.Repository),
dataSourceType: z.literal(SecretScanningDataSource.BitBucket),
dataSourceType: z.literal(SecretScanningDataSource.Bitbucket),
details: GitRepositoryScanFindingDetailsSchema
});
export const BitbucketDataSourceCredentialsSchema = z.object({
webhookId: z.string()
});
@@ -1,11 +1,9 @@
import { PushEvent } from "@octokit/webhooks-types";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
import { logger } from "@app/lib/logger";
import { TBitBucketDataSource } from "./bitbucket-secret-scanning-types";
import { TBitbucketDataSource, TBitbucketPushEvent } from "./bitbucket-secret-scanning-types";
export const bitBucketSecretScanningService = (
secretScanningV2DAL: TSecretScanningV2DALFactory,
@@ -14,18 +12,18 @@ export const bitBucketSecretScanningService = (
const handleInstallationDeletedEvent = async (installationId: number) => {
const dataSource = await secretScanningV2DAL.dataSources.findOne({
externalId: String(installationId),
type: SecretScanningDataSource.BitBucket
type: SecretScanningDataSource.Bitbucket
});
if (!dataSource) {
logger.error(
`secretScanningV2RemoveEvent: BitBucket - Could not find data source [installationId=${installationId}]`
`secretScanningV2RemoveEvent: Bitbucket - Could not find data source [installationId=${installationId}]`
);
return;
}
logger.info(
`secretScanningV2RemoveEvent: BitBucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]`
`secretScanningV2RemoveEvent: Bitbucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]`
);
await secretScanningV2DAL.dataSources.updateById(dataSource.id, {
@@ -33,24 +31,26 @@ export const bitBucketSecretScanningService = (
});
};
const handlePushEvent = async (payload: PushEvent) => {
const { commits, repository, installation } = payload;
const handlePushEvent = async (payload: TBitbucketPushEvent & { dataSourceId: string }) => {
const { push, repository } = payload;
if (!commits || !repository || !installation) {
if (!push?.changes?.length || !repository?.workspace?.uuid) {
logger.warn(
`secretScanningV2PushEvent: BitBucket - Insufficient data [commits=${commits?.length ?? 0}] [repository=${repository.name}] [installationId=${installation?.id}]`
`secretScanningV2PushEvent: Bitbucket - Insufficient data [changes=${
push?.changes?.length ?? 0
}] [repository=${repository?.name}] [workspaceUuid=${repository?.workspace?.uuid}]`
);
return;
}
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
externalId: String(installation.id),
type: SecretScanningDataSource.BitBucket
})) as TBitBucketDataSource | undefined;
externalId: payload.dataSourceId,
type: SecretScanningDataSource.Bitbucket
})) as TBitbucketDataSource | undefined;
if (!dataSource) {
logger.error(
`secretScanningV2PushEvent: BitBucket - Could not find data source [installationId=${installation.id}]`
`secretScanningV2PushEvent: Bitbucket - Could not find data source [workspaceUuid=${repository.workspace.uuid}]`
);
return;
}
@@ -62,20 +62,20 @@ export const bitBucketSecretScanningService = (
if (!isAutoScanEnabled) {
logger.info(
`secretScanningV2PushEvent: BitBucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [installationId=${installation.id}]`
`secretScanningV2PushEvent: Bitbucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [workspaceUuid=${repository.workspace.uuid}]`
);
return;
}
if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) {
await secretScanningV2Queue.queueResourceDiffScan({
dataSourceType: SecretScanningDataSource.BitBucket,
dataSourceType: SecretScanningDataSource.Bitbucket,
payload,
dataSourceId: dataSource.id
});
} else {
logger.info(
`secretScanningV2PushEvent: BitBucket - ignoring due to repository not being present in config [installationId=${installation.id}] [dataSourceId=${dataSource.id}]`
`secretScanningV2PushEvent: Bitbucket - ignoring due to repository not being present in config [workspaceUuid=${repository.workspace.uuid}] [dataSourceId=${dataSource.id}]`
);
}
};
@@ -1,31 +1,84 @@
import { PushEvent } from "@octokit/webhooks-types";
import { z } from "zod";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TBitBucketConnection } from "@app/services/app-connection/bitbucket";
import { TBitbucketConnection } from "@app/services/app-connection/bitbucket";
import {
BitBucketDataSourceListItemSchema,
BitBucketDataSourceSchema,
BitBucketFindingSchema,
CreateBitBucketDataSourceSchema
BitbucketDataSourceCredentialsSchema,
BitbucketDataSourceListItemSchema,
BitbucketDataSourceSchema,
BitbucketFindingSchema,
CreateBitbucketDataSourceSchema
} from "./bitbucket-secret-scanning-schemas";
export type TBitBucketDataSource = z.infer<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 & {
connection: TBitBucketConnection;
export type TBitbucketFinding = z.infer<typeof BitbucketFindingSchema>;
export type TBitbucketDataSourceWithConnection = TBitbucketDataSource & {
connection: TBitbucketConnection;
};
export type TQueueBitBucketResourceDiffScan = {
dataSourceType: SecretScanningDataSource.BitBucket;
payload: PushEvent;
export type TBitbucketPushEventRepository = {
full_name: string;
name: string;
workspace: {
slug: string;
uuid: string;
};
uuid: string;
};
export type TBitbucketPushEventCommit = {
hash: string;
message: string;
author: {
raw: string;
user?: {
display_name: string;
uuid: string;
nickname: string;
};
};
date: string;
};
export type TBitbucketPushEventChange = {
new?: {
name: string;
type: string;
};
old?: {
name: string;
type: string;
};
created: boolean;
closed: boolean;
forced: boolean;
commits: TBitbucketPushEventCommit[];
};
export type TBitbucketPushEvent = {
push: {
changes: TBitbucketPushEventChange[];
};
repository: TBitbucketPushEventRepository;
actor: {
display_name: string;
uuid: string;
nickname: string;
};
};
export type TQueueBitbucketResourceDiffScan = {
dataSourceType: SecretScanningDataSource.Bitbucket;
payload: TBitbucketPushEvent & { dataSourceId: string };
dataSourceId: string;
resourceId: string;
scanId: string;
@@ -1,6 +1,6 @@
export enum SecretScanningDataSource {
GitHub = "github",
BitBucket = "bitbucket"
Bitbucket = "bitbucket"
}
export enum SecretScanningScanStatus {
@@ -1,4 +1,4 @@
import { BitBucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
import { SecretScanningDataSource } from "./secret-scanning-v2-enums";
@@ -17,5 +17,5 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory<
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
[SecretScanningDataSource.BitBucket]: BitBucketSecretScanningFactory as TSecretScanningFactoryImplementation
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation
};
@@ -13,7 +13,7 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte
const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = {
[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 = () => {
@@ -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> = {
[SecretScanningDataSource.GitHub]: "GitHub",
[SecretScanningDataSource.BitBucket]: "BitBucket"
[SecretScanningDataSource.Bitbucket]: "Bitbucket"
};
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.BitBucket]: AppConnection.BitBucket
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
};
export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
[SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" }
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" }
};
@@ -5,12 +5,13 @@ import {
TSecretScanningScans
} from "@app/db/schemas";
import {
TBitBucketDataSource,
TBitBucketDataSourceInput,
TBitBucketDataSourceListItem,
TBitBucketDataSourceWithConnection,
TBitBucketFinding,
TQueueBitBucketResourceDiffScan
TBitbucketDataSource,
TBitbucketDataSourceCredentials,
TBitbucketDataSourceInput,
TBitbucketDataSourceListItem,
TBitbucketDataSourceWithConnection,
TBitbucketFinding,
TQueueBitbucketResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/bitbucket";
import {
TGitHubDataSource,
@@ -27,7 +28,7 @@ import {
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource;
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt?: Date | null;
@@ -51,13 +52,15 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
export type TSecretScanningDataSourceWithConnection =
| TGitHubDataSourceWithConnection
| TBitBucketDataSourceWithConnection;
| TBitbucketDataSourceWithConnection;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitBucketDataSourceInput;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitbucketDataSourceInput;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitBucketDataSourceListItem;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitbucketDataSourceListItem;
export type TSecretScanningFinding = TGitHubFinding | TBitBucketFinding;
export type TDataSourceCredentialsSchema = TBitbucketDataSourceCredentials | undefined;
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding;
export type TListSecretScanningDataSourcesByProjectId = {
projectId: string;
@@ -109,7 +112,7 @@ export type TQueueSecretScanningDataSourceFullScan = {
scanId: string;
};
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitBucketResourceDiffScan;
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan;
export type TQueueSecretScanningSendNotification = {
dataSource: TSecretScanningDataSources;
@@ -149,7 +152,7 @@ export type TSecretScanningDataSourceRaw = NonNullable<
export type TSecretScanningFactoryInitialize<
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
C extends TSecretScanningDataSourceCredentials = undefined
C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema
> = (
params: {
payload: TCreateSecretScanningDataSourceDTO;
@@ -161,7 +164,7 @@ export type TSecretScanningFactoryInitialize<
export type TSecretScanningFactoryPostInitialization<
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
C extends TSecretScanningDataSourceCredentials = undefined
C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema
> = (params: {
payload: TCreateSecretScanningDataSourceDTO;
connection: T;
@@ -196,4 +199,4 @@ export type TUpsertSecretScanningConfigDTO = {
content: string | null;
};
export type TSecretScanningDataSourceCredentials = undefined;
export type TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema;
@@ -1,14 +1,14 @@
import { z } from "zod";
import { BitBucketDataSourceSchema, BitBucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema,
BitBucketDataSourceSchema
BitbucketDataSourceSchema
]);
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
GitHubFindingSchema,
BitBucketFindingSchema
BitbucketFindingSchema
]);
+3 -2
View File
@@ -2270,8 +2270,8 @@ export const AppConnections = {
accessTokenType: "The type of token used to connect with GitLab."
},
BITBUCKET: {
email: "The email used to access BitBucket.",
apiToken: "The API token used to access BitBucket."
email: "The email used to access Bitbucket.",
apiToken: "The API token used to access Bitbucket."
}
}
};
@@ -2634,6 +2634,7 @@ export const SecretScanningDataSources = {
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
},
BITBUCKET: {
workspaceSlug: "The workspace to scan.",
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
}
}
@@ -1,7 +1,9 @@
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";
import { PushEvent } from "@octokit/webhooks-types";
import { Probot } from "probot";
import { z } from "zod";
import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -64,5 +66,31 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
}
});
// TODO(andrey): Register a webhook for BitBucket
// bitbucket push event webhook
server.route({
method: "POST",
url: "/bitbucket",
schema: {
querystring: z.object({
dataSourceId: z.string().min(1, { message: "Data Source ID is required" })
})
},
config: {
rateLimit: writeLimit
},
handler: async (req, res) => {
// TODO(andrey): Verify IP is from bitbucket
const { dataSourceId } = req.query;
if (!dataSourceId) return res.status(400).send({ message: "Data Source ID is required" });
await server.services.secretScanningV2.bitbucket.handlePushEvent({
...(req.body as TBitbucketPushEvent),
dataSourceId
});
return res.send("ok");
}
});
};
@@ -32,8 +32,8 @@ import {
SanitizedAzureKeyVaultConnectionSchema
} from "@app/services/app-connection/azure-key-vault";
import {
BitBucketConnectionListItemSchema,
SanitizedBitBucketConnectionSchema
BitbucketConnectionListItemSchema,
SanitizedBitbucketConnectionSchema
} from "@app/services/app-connection/bitbucket";
import {
CamundaConnectionListItemSchema,
@@ -121,7 +121,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedFlyioConnectionSchema.options,
...SanitizedGitLabConnectionSchema.options,
...SanitizedCloudflareConnectionSchema.options,
...SanitizedBitBucketConnectionSchema.options
...SanitizedBitbucketConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -154,7 +154,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
FlyioConnectionListItemSchema,
GitLabConnectionListItemSchema,
CloudflareConnectionListItemSchema,
BitBucketConnectionListItemSchema
BitbucketConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -4,25 +4,53 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateBitBucketConnectionSchema,
SanitizedBitBucketConnectionSchema,
UpdateBitBucketConnectionSchema
CreateBitbucketConnectionSchema,
SanitizedBitbucketConnectionSchema,
UpdateBitbucketConnectionSchema
} from "@app/services/app-connection/bitbucket";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerBitBucketConnectionRouter = async (server: FastifyZodProvider) => {
export const registerBitbucketConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.BitBucket,
app: AppConnection.Bitbucket,
server,
sanitizedResponseSchema: SanitizedBitBucketConnectionSchema,
createSchema: CreateBitBucketConnectionSchema,
updateSchema: UpdateBitBucketConnectionSchema
sanitizedResponseSchema: SanitizedBitbucketConnectionSchema,
createSchema: CreateBitbucketConnectionSchema,
updateSchema: UpdateBitbucketConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/workspaces`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
workspaces: z.object({ slug: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
params: { connectionId }
} = req;
const workspaces = await server.services.appConnection.bitbucket.listWorkspaces(connectionId, req.permission);
return { workspaces };
}
});
server.route({
method: "GET",
url: `/:connectionId/repositories`,
@@ -33,17 +61,26 @@ export const registerBitBucketConnectionRouter = async (server: FastifyZodProvid
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
workspaceSlug: z.string()
}),
response: {
200: z.object({
repositories: z.object({ id: z.string(), name: z.string() }).array()
repositories: z.object({ slug: z.string(), full_name: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const {
params: { connectionId },
query: { workspaceSlug }
} = req;
const repositories = await server.services.appConnection.bitbucket.listRepositories(connectionId, req.permission);
const repositories = await server.services.appConnection.bitbucket.listRepositories(
{ connectionId, workspaceSlug },
req.permission
);
return { repositories };
}
@@ -9,7 +9,7 @@ import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-confi
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerBitBucketConnectionRouter } from "./bitbucket-connection-router";
import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
@@ -64,5 +64,5 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.BitBucket]: registerBitBucketConnectionRouter
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter
};
@@ -28,7 +28,7 @@ export enum AppConnection {
Flyio = "flyio",
GitLab = "gitlab",
Cloudflare = "cloudflare",
BitBucket = "bitbucket"
Bitbucket = "bitbucket"
}
export enum AWSRegion {
@@ -51,9 +51,9 @@ import {
validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
import {
BitBucketConnectionMethod,
getBitBucketConnectionListItem,
validateBitBucketConnectionCredentials
BitbucketConnectionMethod,
getBitbucketConnectionListItem,
validateBitbucketConnectionCredentials
} from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
@@ -142,7 +142,7 @@ export const listAppConnectionOptions = () => {
getFlyioConnectionListItem(),
getGitLabConnectionListItem(),
getCloudflareConnectionListItem(),
getBitBucketConnectionListItem()
getBitbucketConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -223,7 +223,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.BitBucket]: validateBitBucketConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -260,7 +260,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.APIToken:
case BitBucketConnectionMethod.ApiToken:
case BitbucketConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -341,7 +341,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.BitBucket]: platformManagedCredentialsNotSupported
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
@@ -30,7 +30,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.BitBucket]: "BitBucket"
[AppConnection.Bitbucket]: "Bitbucket"
};
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.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.BitBucket]: AppConnectionPlanType.Regular
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular
};
@@ -45,7 +45,7 @@ import { azureClientSecretsConnectionService } from "./azure-client-secrets/azur
import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas";
import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateBitBucketConnectionCredentialsSchema } from "./bitbucket";
import { ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket";
import { bitBucketConnectionService } from "./bitbucket/bitbucket-connection-service";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service";
@@ -122,7 +122,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.BitBucket]: ValidateBitBucketConnectionCredentialsSchema
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -57,10 +57,10 @@ import {
TValidateAzureKeyVaultConnectionCredentialsSchema
} from "./azure-key-vault";
import {
TBitBucketConnection,
TBitBucketConnectionConfig,
TBitBucketConnectionInput,
TValidateBitBucketConnectionCredentialsSchema
TBitbucketConnection,
TBitbucketConnectionConfig,
TBitbucketConnectionInput,
TValidateBitbucketConnectionCredentialsSchema
} from "./bitbucket";
import {
TCamundaConnection,
@@ -202,7 +202,7 @@ export type TAppConnection = { id: string } & (
| TFlyioConnection
| TGitLabConnection
| TCloudflareConnection
| TBitBucketConnection
| TBitbucketConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -239,7 +239,7 @@ export type TAppConnectionInput = { id: string } & (
| TFlyioConnectionInput
| TGitLabConnectionInput
| TCloudflareConnectionInput
| TBitBucketConnectionInput
| TBitbucketConnectionInput
);
export type TSqlConnectionInput =
@@ -284,7 +284,7 @@ export type TAppConnectionConfig =
| TFlyioConnectionConfig
| TGitLabConnectionConfig
| TCloudflareConnectionConfig
| TBitBucketConnectionConfig;
| TBitbucketConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -316,7 +316,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateFlyioConnectionCredentialsSchema
| TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema
| TValidateBitBucketConnectionCredentialsSchema;
| TValidateBitbucketConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -1,3 +1,3 @@
export enum BitBucketConnectionMethod {
export enum BitbucketConnectionMethod {
ApiToken = "api-token"
}
@@ -5,18 +5,23 @@ import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { BitBucketConnectionMethod } from "./bitbucket-connection-enums";
import { TBitBucketConnection, TBitBucketConnectionConfig, TBitBucketRepo } from "./bitbucket-connection-types";
import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
import {
TBitbucketConnection,
TBitbucketConnectionConfig,
TBitbucketRepo,
TBitbucketWorkspace
} from "./bitbucket-connection-types";
export const getBitBucketConnectionListItem = () => {
export const getBitbucketConnectionListItem = () => {
return {
name: "BitBucket" as const,
app: AppConnection.BitBucket as const,
methods: Object.values(BitBucketConnectionMethod) as [BitBucketConnectionMethod.ApiToken]
name: "Bitbucket" as const,
app: AppConnection.Bitbucket as const,
methods: Object.values(BitbucketConnectionMethod) as [BitbucketConnectionMethod.ApiToken]
};
};
export const getBitBucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => {
export const getBitbucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => {
try {
const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, {
headers: {
@@ -38,12 +43,17 @@ export const getBitBucketUser = async ({ email, apiToken }: { email: string; api
}
};
export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => {
await getBitBucketUser(config.credentials);
export const validateBitbucketConnectionCredentials = async (config: TBitbucketConnectionConfig) => {
await getBitbucketUser(config.credentials);
return config.credentials;
};
export const listBitBucketRepositories = async (appConnection: TBitBucketConnection) => {
interface BitbucketWorkspacesResponse {
values: TBitbucketWorkspace[];
next?: string;
}
export const listBitbucketWorkspaces = async (appConnection: TBitbucketConnection) => {
const { email, apiToken } = appConnection.credentials;
const headers = {
@@ -51,19 +61,52 @@ export const listBitBucketRepositories = async (appConnection: TBitBucketConnect
Accept: "application/json"
};
let allRepos: TBitBucketRepo[] = [];
let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`;
let allWorkspaces: TBitbucketWorkspace[] = [];
let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces?pagelen=100`;
let iterationCount = 0;
// Limit to 10 iterations, fetching at most 10 * 100 = 1000 workspaces
while (nextUrl && iterationCount < 10) {
// eslint-disable-next-line no-await-in-loop
const { data }: { data: BitbucketWorkspacesResponse } = await request.get<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;
// Limit to 10 iterations, fetching at most 10 * 100 = 1000 repositories
while (nextUrl && iterationCount < 10) {
// eslint-disable-next-line no-await-in-loop
const { data }: { data: { values: TBitBucketRepo[]; next?: string } } = await request.get<{
values: TBitBucketRepo[];
next?: string;
}>(nextUrl, {
headers
});
const { data }: { data: BitbucketRepositoriesResponse } = await request.get<BitbucketRepositoriesResponse>(
nextUrl,
{
headers
}
);
allRepos = allRepos.concat(data.values);
nextUrl = data.next;
@@ -8,54 +8,54 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { BitBucketConnectionMethod } from "./bitbucket-connection-enums";
import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
export const BitBucketConnectionAccessTokenCredentialsSchema = z.object({
export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.BITBUCKET.apiToken),
email: z.string().email().trim().min(1, "Email required").describe(AppConnections.CREDENTIALS.BITBUCKET.email)
});
const BaseBitBucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.BitBucket) });
const BaseBitbucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Bitbucket) });
export const BitBucketConnectionSchema = BaseBitBucketConnectionSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken),
credentials: BitBucketConnectionAccessTokenCredentialsSchema
export const BitbucketConnectionSchema = BaseBitbucketConnectionSchema.extend({
method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: BitbucketConnectionAccessTokenCredentialsSchema
});
export const SanitizedBitBucketConnectionSchema = z.discriminatedUnion("method", [
BaseBitBucketConnectionSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken),
credentials: BitBucketConnectionAccessTokenCredentialsSchema.pick({
export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method", [
BaseBitbucketConnectionSchema.extend({
method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({
email: true
})
})
]);
export const ValidateBitBucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(BitBucketConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.BitBucket).method),
credentials: BitBucketConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.BitBucket).credentials
.literal(BitbucketConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.Bitbucket).method),
credentials: BitbucketConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Bitbucket).credentials
)
})
]);
export const CreateBitBucketConnectionSchema = ValidateBitBucketConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.BitBucket)
export const CreateBitbucketConnectionSchema = ValidateBitbucketConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Bitbucket)
);
export const UpdateBitBucketConnectionSchema = z
export const UpdateBitbucketConnectionSchema = z
.object({
credentials: BitBucketConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.BitBucket).credentials
credentials: BitbucketConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Bitbucket).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.BitBucket));
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket));
export const BitBucketConnectionListItemSchema = z.object({
name: z.literal("BitBucket"),
app: z.literal(AppConnection.BitBucket),
methods: z.nativeEnum(BitBucketConnectionMethod).array()
export const BitbucketConnectionListItemSchema = z.object({
name: z.literal("Bitbucket"),
app: z.literal(AppConnection.Bitbucket),
methods: z.nativeEnum(BitbucketConnectionMethod).array()
});
@@ -1,25 +1,33 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listBitBucketRepositories } from "./bitbucket-connection-fns";
import { TBitBucketConnection } from "./bitbucket-connection-types";
import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns";
import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TBitBucketConnection>;
) => Promise<TBitbucketConnection>;
export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.BitBucket, connectionId, actor);
const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor);
const workspaces = await listBitbucketWorkspaces(appConnection);
return workspaces;
};
const repositories = await listBitBucketRepositories(appConnection);
return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name }));
const listRepositories = async (
{ connectionId, workspaceSlug }: TGetBitbucketRepositoriesDTO,
actor: OrgServiceActor
) => {
const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor);
const repositories = await listBitbucketRepositories(appConnection, workspaceSlug);
return repositories;
};
return {
listWorkspaces,
listRepositories
};
};
@@ -4,27 +4,36 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
BitBucketConnectionSchema,
CreateBitBucketConnectionSchema,
ValidateBitBucketConnectionCredentialsSchema
BitbucketConnectionSchema,
CreateBitbucketConnectionSchema,
ValidateBitbucketConnectionCredentialsSchema
} from "./bitbucket-connection-schemas";
export type TBitBucketConnection = z.infer<typeof BitBucketConnectionSchema>;
export type TBitbucketConnection = z.infer<typeof BitbucketConnectionSchema>;
export type TBitBucketConnectionInput = z.infer<typeof CreateBitBucketConnectionSchema> & {
app: AppConnection.BitBucket;
export type TBitbucketConnectionInput = z.infer<typeof CreateBitbucketConnectionSchema> & {
app: AppConnection.Bitbucket;
};
export type TValidateBitBucketConnectionCredentialsSchema = typeof ValidateBitBucketConnectionCredentialsSchema;
export type TValidateBitbucketConnectionCredentialsSchema = typeof ValidateBitbucketConnectionCredentialsSchema;
export type TBitBucketConnectionConfig = DiscriminativePick<
TBitBucketConnectionInput,
export type TBitbucketConnectionConfig = DiscriminativePick<
TBitbucketConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TBitBucketRepo = {
export type TGetBitbucketRepositoriesDTO = {
connectionId: string;
workspaceSlug: string;
};
export type TBitbucketWorkspace = {
slug: string;
};
export type TBitbucketRepo = {
full_name: string; // workspace-slug/repo-slug
slug: string;
};
@@ -814,9 +814,9 @@ const getAppsCloudflareWorkers = async ({ accessToken, accountId }: { accessToke
};
/**
* Return list of repositories for the BitBucket integration based on provided BitBucket workspace
* Return list of repositories for the Bitbucket integration based on provided Bitbucket workspace
*/
const getAppsBitBucket = async ({ accessToken, workspaceSlug }: { accessToken: string; workspaceSlug?: string }) => {
const getAppsBitbucket = async ({ accessToken, workspaceSlug }: { accessToken: string; workspaceSlug?: string }) => {
interface RepositoriesResponse {
size: number;
page: number;
@@ -1302,7 +1302,7 @@ export const getApps = async ({
});
case Integrations.BITBUCKET:
return getAppsBitBucket({
return getAppsBitbucket({
accessToken,
workspaceSlug
});
@@ -342,7 +342,7 @@ export const getIntegrationOptions = async () => {
{
name: "Bitbucket",
slug: "bitbucket",
image: "BitBucket.png",
image: "Bitbucket.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_BITBUCKET,
@@ -3921,9 +3921,9 @@ const syncSecretsCloudflareWorkers = async ({
};
/**
* Sync/push [secrets] to BitBucket repo with name [integration.app]
* Sync/push [secrets] to Bitbucket repo with name [integration.app]
*/
const syncSecretsBitBucket = async ({
const syncSecretsBitbucket = async ({
integration,
secrets,
accessToken
@@ -4832,7 +4832,7 @@ export const syncIntegrationSecrets = async ({
});
break;
case Integrations.BITBUCKET:
await syncSecretsBitBucket({
await syncSecretsBitbucket({
integration,
secrets,
accessToken
@@ -64,7 +64,7 @@ type ExchangeCodeGitlabResponse = {
created_at: number;
};
type ExchangeCodeBitBucketResponse = {
type ExchangeCodeBitbucketResponse = {
access_token: string;
token_type: string;
expires_in: number;
@@ -392,10 +392,10 @@ const exchangeCodeGitlab = async ({ code, url }: { code: string; url?: string })
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Bitbucket
* code-token exchange
*/
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
const exchangeCodeBitbucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
@@ -403,7 +403,7 @@ const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
}
const res = (
await request.post<ExchangeCodeBitBucketResponse>(
await request.post<ExchangeCodeBitbucketResponse>(
IntegrationUrls.BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
@@ -490,7 +490,7 @@ export const exchangeCode = async ({
url
});
case Integrations.BITBUCKET:
return exchangeCodeBitBucket({
return exchangeCodeBitbucket({
code
});
default:
@@ -524,7 +524,7 @@ type RefreshTokenGitLabResponse = {
created_at: number;
};
type RefreshTokenBitBucketResponse = {
type RefreshTokenBitbucketResponse = {
access_token: string;
token_type: string;
expires_in: number;
@@ -653,9 +653,9 @@ const exchangeRefreshGitLab = async ({ refreshToken, url }: { url?: string | nul
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* BitBucket integration
* Bitbucket integration
*/
const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string }) => {
const exchangeRefreshBitbucket = async ({ refreshToken }: { refreshToken: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
@@ -664,7 +664,7 @@ const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string
const {
data
}: {
data: RefreshTokenBitBucketResponse;
data: RefreshTokenBitbucketResponse;
} = await request.post(
IntegrationUrls.BITBUCKET_TOKEN_URL,
new URLSearchParams({
@@ -794,7 +794,7 @@ export const exchangeRefresh = async (
url
});
case Integrations.BITBUCKET:
return exchangeRefreshBitBucket({
return exchangeRefreshBitbucket({
refreshToken
});
case Integrations.GCP_SECRET_MANAGER: