From 62ad82f7b1292c52d0af5ebd9a135f46a9ed0470 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 2 Jul 2025 17:56:48 -0400 Subject: [PATCH 01/32] feat(app-connection): BitBucket app connection --- backend/src/lib/api-docs/constants.ts | 4 + .../app-connection-router.ts | 10 +- .../bitbucket-connection-router.ts | 18 +++ .../routes/v1/app-connection-routers/index.ts | 4 +- .../app-connection/app-connection-enums.ts | 3 +- .../app-connection/app-connection-fns.ts | 15 +- .../app-connection/app-connection-maps.ts | 6 +- .../app-connection/app-connection-service.ts | 8 +- .../app-connection/app-connection-types.ts | 14 +- .../bitbucket/bitbucket-connection-enums.ts | 3 + .../bitbucket/bitbucket-connection-fns.ts | 40 +++++ .../bitbucket/bitbucket-connection-schemas.ts | 61 ++++++++ .../bitbucket/bitbucket-connection-service.ts | 15 ++ .../bitbucket/bitbucket-connection-types.ts | 38 +++++ .../app-connection/bitbucket/index.ts | 4 + frontend/src/helpers/appConnections.ts | 5 +- .../api/appConnections/bitbucket/index.ts | 2 + .../api/appConnections/bitbucket/queries.tsx | 0 .../api/appConnections/bitbucket/types.ts | 0 .../src/hooks/api/appConnections/enums.ts | 3 +- .../api/appConnections/types/app-options.ts | 8 +- .../types/bitbucket-connection.ts | 14 ++ .../hooks/api/appConnections/types/index.ts | 6 +- .../AppConnectionForm/AppConnectionForm.tsx | 5 + .../BitBucketConnectionForm.tsx | 144 ++++++++++++++++++ 25 files changed, 413 insertions(+), 17 deletions(-) create mode 100644 backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts create mode 100644 backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts create mode 100644 backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts create mode 100644 backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts create mode 100644 backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts create mode 100644 backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts create mode 100644 backend/src/services/app-connection/bitbucket/index.ts create mode 100644 frontend/src/hooks/api/appConnections/bitbucket/index.ts create mode 100644 frontend/src/hooks/api/appConnections/bitbucket/queries.tsx create mode 100644 frontend/src/hooks/api/appConnections/bitbucket/types.ts create mode 100644 frontend/src/hooks/api/appConnections/types/bitbucket-connection.ts create mode 100644 frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/BitBucketConnectionForm.tsx diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 4405728b2..b457eafb3 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2268,6 +2268,10 @@ export const AppConnections = { accessToken: "The Access Token used to access GitLab.", code: "The OAuth code to use to connect with GitLab.", 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." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 6160828f4..be991703c 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -31,6 +31,10 @@ import { AzureKeyVaultConnectionListItemSchema, SanitizedAzureKeyVaultConnectionSchema } from "@app/services/app-connection/azure-key-vault"; +import { + BitBucketConnectionListItemSchema, + SanitizedBitBucketConnectionSchema +} from "@app/services/app-connection/bitbucket"; import { CamundaConnectionListItemSchema, SanitizedCamundaConnectionSchema @@ -116,7 +120,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedRenderConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options, - ...SanitizedCloudflareConnectionSchema.options + ...SanitizedCloudflareConnectionSchema.options, + ...SanitizedBitBucketConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -148,7 +153,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ RenderConnectionListItemSchema, FlyioConnectionListItemSchema, GitLabConnectionListItemSchema, - CloudflareConnectionListItemSchema + CloudflareConnectionListItemSchema, + BitBucketConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts new file mode 100644 index 000000000..bcef795c1 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateBitBucketConnectionSchema, + SanitizedBitBucketConnectionSchema, + UpdateBitBucketConnectionSchema +} from "@app/services/app-connection/bitbucket"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerBitBucketConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.BitBucket, + server, + sanitizedResponseSchema: SanitizedBitBucketConnectionSchema, + createSchema: CreateBitBucketConnectionSchema, + updateSchema: UpdateBitBucketConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index cd4ccd728..e164a9088 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -9,6 +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 { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; @@ -62,5 +63,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { getRenderConnectionListItem(), getFlyioConnectionListItem(), getGitLabConnectionListItem(), - getCloudflareConnectionListItem() + getCloudflareConnectionListItem(), + getBitBucketConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -216,7 +222,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.BitBucket]: validateBitBucketConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -253,6 +260,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.APIToken: + case BitBucketConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -332,7 +340,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Render]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, - [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported + [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, + [AppConnection.BitBucket]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 9c0a3b5b8..a2d953d07 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -29,7 +29,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Render]: "Render", [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", - [AppConnection.Cloudflare]: "Cloudflare" + [AppConnection.Cloudflare]: "Cloudflare", + [AppConnection.BitBucket]: "BitBucket" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -61,5 +62,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -232,6 +239,7 @@ export type TAppConnectionInput = { id: string } & ( | TFlyioConnectionInput | TGitLabConnectionInput | TCloudflareConnectionInput + | TBitBucketConnectionInput ); export type TSqlConnectionInput = @@ -275,7 +283,8 @@ export type TAppConnectionConfig = | TRenderConnectionConfig | TFlyioConnectionConfig | TGitLabConnectionConfig - | TCloudflareConnectionConfig; + | TCloudflareConnectionConfig + | TBitBucketConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -306,7 +315,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateRenderConnectionCredentialsSchema | TValidateFlyioConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema - | TValidateCloudflareConnectionCredentialsSchema; + | TValidateCloudflareConnectionCredentialsSchema + | TValidateBitBucketConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts new file mode 100644 index 000000000..629c5b6d7 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts @@ -0,0 +1,3 @@ +export enum BitBucketConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts new file mode 100644 index 000000000..45ce2c0b5 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -0,0 +1,40 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; +import { TBitBucketConnectionConfig } from "./bitbucket-connection-types"; + +export const getBitBucketConnectionListItem = () => { + return { + name: "BitBucket" as const, + app: AppConnection.BitBucket as const, + methods: Object.values(BitBucketConnectionMethod) as [BitBucketConnectionMethod.ApiToken] + }; +}; + +export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => { + const { email, apiToken } = config.credentials; + + try { + await request.get("https://api.bitbucket.org/2.0/user", { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts new file mode 100644 index 000000000..a331db8cc --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts @@ -0,0 +1,61 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; + +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) }); + +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({ + email: true + }) + }) +]); + +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 + ) + }) +]); + +export const CreateBitBucketConnectionSchema = ValidateBitBucketConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.BitBucket) +); + +export const UpdateBitBucketConnectionSchema = z + .object({ + credentials: BitBucketConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.BitBucket).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.BitBucket)); + +export const BitBucketConnectionListItemSchema = z.object({ + name: z.literal("BitBucket"), + app: z.literal(AppConnection.BitBucket), + methods: z.nativeEnum(BitBucketConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts new file mode 100644 index 000000000..16d3edea4 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -0,0 +1,15 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +// import { listBitBucketVaults } from "./bitbucket-connection-fns"; +import { TBitBucketConnection } from "./bitbucket-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const bitBucketConnectionService = (_getAppConnection: TGetAppConnectionFunc) => { + return {}; +}; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts new file mode 100644 index 000000000..a59fdb397 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -0,0 +1,38 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + BitBucketConnectionSchema, + CreateBitBucketConnectionSchema, + ValidateBitBucketConnectionCredentialsSchema +} from "./bitbucket-connection-schemas"; + +export type TBitBucketConnection = z.infer; + +export type TBitBucketConnectionInput = z.infer & { + app: AppConnection.BitBucket; +}; + +export type TValidateBitBucketConnectionCredentialsSchema = typeof ValidateBitBucketConnectionCredentialsSchema; + +export type TBitBucketConnectionConfig = DiscriminativePick< + TBitBucketConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TBitBucketVault = { + id: string; + name: string; + type: string; + items: number; + + attributeVersion: number; + contentVersion: number; + + createdAt: string; + updatedAt: string; +}; diff --git a/backend/src/services/app-connection/bitbucket/index.ts b/backend/src/services/app-connection/bitbucket/index.ts new file mode 100644 index 000000000..634342ba7 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/index.ts @@ -0,0 +1,4 @@ +export * from "./bitbucket-connection-enums"; +export * from "./bitbucket-connection-fns"; +export * from "./bitbucket-connection-schemas"; +export * from "./bitbucket-connection-types"; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index e1fd9e365..972247bf4 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -42,6 +42,7 @@ import { import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; +import { BitBucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -88,7 +89,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Render]: { name: "Render", image: "Render.png" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [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" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -120,6 +122,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.ApiToken: + case BitBucketConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/hooks/api/appConnections/bitbucket/index.ts b/frontend/src/hooks/api/appConnections/bitbucket/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/bitbucket/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/hooks/api/appConnections/bitbucket/types.ts b/frontend/src/hooks/api/appConnections/bitbucket/types.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 8097720a7..87e08ae36 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -27,5 +27,6 @@ export enum AppConnection { Render = "render", Flyio = "flyio", Gitlab = "gitlab", - Cloudflare = "cloudflare" + Cloudflare = "cloudflare", + BitBucket = "bitbucket" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index b71370da7..84af3dbe6 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -132,6 +132,10 @@ export type TCloudflareConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Cloudflare; }; +export type TBitBucketConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.BitBucket; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -159,7 +163,8 @@ export type TAppConnectionOption = | TRenderConnectionOption | TFlyioConnectionOption | TGitlabConnectionOption - | TCloudflareConnectionOption; + | TCloudflareConnectionOption + | TBitBucketConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -191,4 +196,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Flyio]: TFlyioConnectionOption; [AppConnection.Gitlab]: TGitlabConnectionOption; [AppConnection.Cloudflare]: TCloudflareConnectionOption; + [AppConnection.BitBucket]: TBitBucketConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/bitbucket-connection.ts b/frontend/src/hooks/api/appConnections/types/bitbucket-connection.ts new file mode 100644 index 000000000..5a4740105 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/bitbucket-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum BitBucketConnectionMethod { + ApiToken = "api-token" +} + +export type TBitBucketConnection = TRootAppConnection & { app: AppConnection.BitBucket } & { + method: BitBucketConnectionMethod.ApiToken; + credentials: { + email: string; + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 2eaebb45a..6c0da9b69 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -7,6 +7,7 @@ import { TAzureAppConfigurationConnection } from "./azure-app-configuration-conn import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection"; import { TAzureDevOpsConnection } from "./azure-devops-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; +import { TBitBucketConnection } from "./bitbucket-connection"; import { TCamundaConnection } from "./camunda-connection"; import { TCloudflareConnection } from "./cloudflare-connection"; import { TDatabricksConnection } from "./databricks-connection"; @@ -59,6 +60,7 @@ export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; +export * from "./bitbucket-connection"; export type TAppConnection = | TAwsConnection @@ -89,7 +91,8 @@ export type TAppConnection = | TRenderConnection | TFlyioConnection | TGitLabConnection - | TCloudflareConnection; + | TCloudflareConnection + | TBitBucketConnection; export type TAvailableAppConnection = Pick; @@ -146,4 +149,5 @@ export type TAppConnectionMap = { [AppConnection.Flyio]: TFlyioConnection; [AppConnection.Gitlab]: TGitLabConnection; [AppConnection.Cloudflare]: TCloudflareConnection; + [AppConnection.BitBucket]: TBitBucketConnection; }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 52abfee5d..92b23ce3e 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -38,6 +38,7 @@ import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; +import { BitBucketConnectionForm } from "./BitBucketConnectionForm"; type FormProps = { onComplete: (appConnection: TAppConnection) => void; @@ -134,6 +135,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.BitBucket: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -228,6 +231,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.BitBucket: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/BitBucketConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/BitBucketConnectionForm.tsx new file mode 100644 index 000000000..e700d7c08 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/BitBucketConnectionForm.tsx @@ -0,0 +1,144 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { BitBucketConnectionMethod, TBitBucketConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TBitBucketConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.BitBucket) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(BitBucketConnectionMethod.ApiToken), + credentials: z.object({ + email: z.string().email().trim().min(1, "Email required"), + apiToken: z.string().trim().min(1, "API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const BitBucketConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.BitBucket, + method: BitBucketConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; From ae35a863bc6b3f19a70df38eb301500eec24951a Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 3 Jul 2025 00:00:50 -0400 Subject: [PATCH 02/32] App connection updates --- backend/src/lib/api-docs/constants.ts | 3 ++ .../bitbucket-connection-router.ts | 33 +++++++++++++++++ .../bitbucket/bitbucket-connection-fns.ts | 31 +++++++++++++--- .../bitbucket/bitbucket-connection-service.ts | 17 +++++++-- .../bitbucket/bitbucket-connection-types.ts | 14 ++----- frontend/src/helpers/appConnections.ts | 2 +- .../api/appConnections/bitbucket/queries.tsx | 37 +++++++++++++++++++ .../api/appConnections/bitbucket/types.ts | 8 ++++ .../hooks/api/appConnections/types/index.ts | 2 +- .../AppConnectionForm/AppConnectionForm.tsx | 2 +- 10 files changed, 127 insertions(+), 22 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b457eafb3..e5ed91a26 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2632,6 +2632,9 @@ export const SecretScanningDataSources = { CONFIG: { GITHUB: { includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' + }, + BITBUCKET: { + includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts index bcef795c1..4f4271273 100644 --- a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -1,9 +1,14 @@ +import { z } from "zod"; + +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 } from "@app/services/app-connection/bitbucket"; +import { AuthMode } from "@app/services/auth/auth-type"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; @@ -15,4 +20,32 @@ export const registerBitBucketConnectionRouter = async (server: FastifyZodProvid createSchema: CreateBitBucketConnectionSchema, updateSchema: UpdateBitBucketConnectionSchema }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/repositories`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + repositories: z.object({ id: z.string(), name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const repositories = await server.services.appConnection.bitbucket.listRepositories(connectionId, req.permission); + + return { repositories }; + } + }); }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts index 45ce2c0b5..11e447443 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -3,9 +3,10 @@ import { AxiosError } from "axios"; import { request } from "@app/lib/config/request"; 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 { TBitBucketConnectionConfig } from "./bitbucket-connection-types"; +import { TBitBucketConnection, TBitBucketConnectionConfig, TBitBucketRepo } from "./bitbucket-connection-types"; export const getBitBucketConnectionListItem = () => { return { @@ -15,16 +16,16 @@ export const getBitBucketConnectionListItem = () => { }; }; -export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => { - const { email, apiToken } = config.credentials; - +export const getBitBucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => { try { - await request.get("https://api.bitbucket.org/2.0/user", { + const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, { headers: { Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, Accept: "application/json" } }); + + return data; } catch (error: unknown) { if (error instanceof AxiosError) { throw new BadRequestError({ @@ -35,6 +36,26 @@ export const validateBitBucketConnectionCredentials = async (config: TBitBucketC message: "Unable to validate connection: verify credentials" }); } +}; +export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => { + await getBitBucketUser(config.credentials); return config.credentials; }; + +export const listBitBucketRepositories = async (appConnection: TBitBucketConnection) => { + const { email, apiToken } = appConnection.credentials; + + // TODO(andrey): Support pagination for cases where a token has access to over 100 repos + const { data } = await request.get<{ values: TBitBucketRepo[] }>( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`, + { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + } + } + ); + + return data.values; +}; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts index 16d3edea4..5b9c049f7 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -1,7 +1,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -// import { listBitBucketVaults } from "./bitbucket-connection-fns"; +import { listBitBucketRepositories } from "./bitbucket-connection-fns"; import { TBitBucketConnection } from "./bitbucket-connection-types"; type TGetAppConnectionFunc = ( @@ -10,6 +10,17 @@ type TGetAppConnectionFunc = ( actor: OrgServiceActor ) => Promise; -export const bitBucketConnectionService = (_getAppConnection: TGetAppConnectionFunc) => { - return {}; +export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.BitBucket, connectionId, actor); + + const repositories = await listBitBucketRepositories(appConnection); + + // TODO(andrey): May need to change from slug to ID or something + return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name })); + }; + + return { + listRepositories + }; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts index a59fdb397..41fdaef29 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -24,15 +24,7 @@ export type TBitBucketConnectionConfig = DiscriminativePick< orgId: string; }; -export type TBitBucketVault = { - id: string; - name: string; - type: string; - items: number; - - attributeVersion: number; - contentVersion: number; - - createdAt: string; - updatedAt: string; +export type TBitBucketRepo = { + full_name: string; // workspace-slug/repo-slug + slug: string; }; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 972247bf4..8ed28f4ab 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -39,10 +39,10 @@ import { VercelConnectionMethod, WindmillConnectionMethod } from "@app/hooks/api/appConnections/types"; +import { BitBucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; -import { BitBucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, diff --git a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx index e69de29bb..6d29d0f64 100644 --- a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx +++ b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TBitBucketConnectionListRepositoriesResponse, TBitBucketRepo } from "./types"; + +const bitBucketConnectionKeys = { + all: [...appConnectionKeys.all, "bitbucket"] as const, + listRepos: (connectionId: string) => + [...bitBucketConnectionKeys.all, "repos", connectionId] as const +}; + +export const useBitBucketConnectionListRepositories = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TBitBucketRepo[], + unknown, + TBitBucketRepo[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: bitBucketConnectionKeys.listRepos(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/bitbucket/${connectionId}/repositories` + ); + + return data.repositories; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/bitbucket/types.ts b/frontend/src/hooks/api/appConnections/bitbucket/types.ts index e69de29bb..c4c02c098 100644 --- a/frontend/src/hooks/api/appConnections/bitbucket/types.ts +++ b/frontend/src/hooks/api/appConnections/bitbucket/types.ts @@ -0,0 +1,8 @@ +export type TBitBucketRepo = { + id: string; + name: string; // workspace-slug/repo-slug +}; + +export type TBitBucketConnectionListRepositoriesResponse = { + repositories: TBitBucketRepo[]; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 6c0da9b69..e42c067d8 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -38,6 +38,7 @@ export * from "./azure-app-configuration-connection"; export * from "./azure-client-secrets-connection"; export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; +export * from "./bitbucket-connection"; export * from "./camunda-connection"; export * from "./cloudflare-connection"; export * from "./databricks-connection"; @@ -60,7 +61,6 @@ export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; -export * from "./bitbucket-connection"; export type TAppConnection = | TAwsConnection diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 92b23ce3e..37cd7a13a 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -16,6 +16,7 @@ import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConn import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm"; import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm"; import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; +import { BitBucketConnectionForm } from "./BitBucketConnectionForm"; import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; @@ -38,7 +39,6 @@ import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; -import { BitBucketConnectionForm } from "./BitBucketConnectionForm"; type FormProps = { onComplete: (appConnection: TAppConnection) => void; From edecfb1f62cc76a48fbba08edf12a415774fa7a7 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 3 Jul 2025 00:01:37 -0400 Subject: [PATCH 03/32] feat(secret-scanning): BitBucket data source --- .../bitbucket-secret-scanning-router.ts | 16 ++ .../v2/secret-scanning-v2-routers/index.ts | 4 +- .../secret-scanning-v2-router.ts | 6 +- .../bitbucket-secret-scanning-constants.ts | 9 + .../bitbucket-secret-scanning-factory.ts | 252 ++++++++++++++++++ .../bitbucket-secret-scanning-schemas.ts | 87 ++++++ .../bitbucket-secret-scanning-service.ts | 87 ++++++ .../bitbucket-secret-scanning-types.ts | 32 +++ .../secret-scanning-v2/bitbucket/index.ts | 3 + .../secret-scanning-v2-enums.ts | 3 +- .../secret-scanning-v2-factory.ts | 4 +- .../secret-scanning-v2-fns.ts | 4 +- .../secret-scanning-v2-maps.ts | 10 +- .../secret-scanning-v2-service.ts | 4 +- .../secret-scanning-v2-types.ts | 22 +- .../secret-scanning-v2-union-schemas.ts | 11 +- .../BitBucketDataSourceConfigFields.tsx | 122 +++++++++ .../SecretScanningDataSourceConfigFields.tsx | 4 +- .../BitBucketDataSourceReviewFields.tsx | 27 ++ .../SecretScanningDataSourceReviewFields.tsx | 4 +- .../schemas/bitbucket-data-source-schema.ts | 18 ++ .../secret-scanning/forms/schemas/index.ts | 4 +- frontend/src/helpers/secretScanningV2.ts | 15 +- .../src/hooks/api/secretScanningV2/enums.ts | 3 +- .../types/bitbucket-data-source.ts | 17 ++ .../hooks/api/secretScanningV2/types/index.ts | 5 +- frontend/src/pages/project/layout.tsx | 42 ++- .../redirects/redirect-approval-page.tsx | 14 +- .../BitBucketDataSourceConfigDisplay.tsx | 18 ++ .../DataSourceConfigDisplay.tsx | 3 + 30 files changed, 796 insertions(+), 54 deletions(-) create mode 100644 backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts create mode 100644 backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts create mode 100644 backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts create mode 100644 backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts create mode 100644 backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts create mode 100644 backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts create mode 100644 backend/src/ee/services/secret-scanning-v2/bitbucket/index.ts create mode 100644 frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx create mode 100644 frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/BitBucketDataSourceReviewFields.tsx create mode 100644 frontend/src/components/secret-scanning/forms/schemas/bitbucket-data-source-schema.ts create mode 100644 frontend/src/hooks/api/secretScanningV2/types/bitbucket-data-source.ts create mode 100644 frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/BitBucketDataSourceConfigDisplay.tsx diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts new file mode 100644 index 000000000..8aa7887e4 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts @@ -0,0 +1,16 @@ +import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints"; +import { + 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) => + registerSecretScanningEndpoints({ + type: SecretScanningDataSource.BitBucket, + server, + responseSchema: BitBucketDataSourceSchema, + createSchema: CreateBitBucketDataSourceSchema, + updateSchema: UpdateBitBucketDataSourceSchema + }); diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts index 703529947..ff7c304f6 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts @@ -1,5 +1,6 @@ import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; +import { registerBitBucketSecretScanningRouter } from "./bitbucket-secret-scanning-router"; import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router"; export * from "./secret-scanning-v2-router"; @@ -8,5 +9,6 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record< SecretScanningDataSource, (server: FastifyZodProvider) => Promise > = { - [SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter + [SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter, + [SecretScanningDataSource.BitBucket]: registerBitBucketSecretScanningRouter }; diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts index 929a60df5..ed7b66a19 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts @@ -2,6 +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 { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github"; import { SecretScanningFindingStatus, @@ -21,7 +22,10 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [GitHubDataSourceListItemSchema]); +const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [ + GitHubDataSourceListItemSchema, + BitBucketDataSourceListItemSchema +]); export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts new file mode 100644 index 000000000..0b9335915 --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts @@ -0,0 +1,9 @@ +import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; +import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types"; +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 +}; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts new file mode 100644 index 000000000..29ffaffaa --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts @@ -0,0 +1,252 @@ +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"; +import { + cloneRepository, + convertPatchLineToFileLineNumber, + replaceNonChangesWithNewlines +} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns"; +import { + TSecretScanningFactoryGetDiffScanFindingsPayload, + TSecretScanningFactoryGetDiffScanResourcePayload, + TSecretScanningFactoryGetFullScanPath, + TSecretScanningFactoryInitialize, + TSecretScanningFactoryListRawResources, + TSecretScanningFactoryPostInitialization +} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types"; +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 +} from "@app/services/app-connection/bitbucket"; + +import { TBitBucketDataSourceWithConnection, TQueueBitBucketResourceDiffScan } from "./bitbucket-secret-scanning-types"; + +export const BitBucketSecretScanningFactory = () => { + const initialize: TSecretScanningFactoryInitialize = async ( + { connection, secretScanningV2DAL }, + callback + ) => { + // TODO(andrey): Swap for something proper + const externalId = connection.credentials.email; + + 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}"` + }); + + return callback({ + externalId + }); + }; + + const postInitialization: TSecretScanningFactoryPostInitialization = async () => { + // no post-initialization required + }; + + const listRawResources: TSecretScanningFactoryListRawResources = async ( + dataSource + ) => { + const { + connection, + config: { includeRepos } + } = dataSource; + + const repos = await listBitBucketRepositories(connection); + + const filteredRepos: typeof repos = []; + if (includeRepos.includes("*")) { + filteredRepos.push(...repos); + } else { + filteredRepos.push(...repos.filter((repo) => includeRepos.includes(repo.full_name))); + } + + return filteredRepos.map(({ slug, full_name }) => ({ + name: full_name, + externalId: slug.toString(), + type: SecretScanningResource.Repository + })); + }; + + // TODO(andrey): Finish + const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ + dataSource, + resourceName, + tempFolder + }) => { + const { + connection: { + credentials: { apiToken, email } + } + } = dataSource; + + const repoPath = join(tempFolder, "repo.git"); + + if (!GitHubRepositoryRegex.test(resourceName)) { + throw new Error("Invalid BitBucket repository name"); + } + + const { username } = await getBitBucketUser({ email, apiToken }); + + await cloneRepository({ + cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`, + repoPath + }); + + return repoPath; + }; + + const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload< + TQueueBitBucketResourceDiffScan["payload"] + > = ({ repository }) => { + return { + name: repository.full_name, + externalId: repository.id.toString(), + type: SecretScanningResource.Repository + }; + }; + + const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload< + TBitBucketDataSourceWithConnection, + TQueueBitBucketResourceDiffScan["payload"] + > = async ({ dataSource, payload, resourceName, configPath }) => { + const { + connection: { + credentials: { apiToken, email } + } + } = dataSource; + + console.log("getDiffScanFindingsPayload"); + + const { commits, 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" + } + }); + + // 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; + + // eslint-disable-next-line no-await-in-loop + const { data: patch } = await request.get( + `https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.id}`, + { + params: { + path: filePath + }, + headers: { + Authorization: authHeader + }, + responseType: "text" + } + ); + + console.log(1); + + // eslint-disable-next-line no-continue + if (!patch) continue; + console.log(2); + + // eslint-disable-next-line + const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); + console.log(3); + console.log(findings); + + 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 + + + console.log("finding"); + console.log(finding.Link); + + 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}` + }; + }); + + console.log("adjusted"); + console.log(adjustedFindings); + + allFindings.push(...adjustedFindings); + } + } + } + + console.log("HEREEE"); + console.log(allFindings); + + return allFindings.map( + ({ + // discard match and secret as we don't want to store + Match, + Secret, + ...finding + }) => ({ + details: titleCaseToCamelCase(finding), + fingerprint: finding.Fingerprint, + severity: SecretScanningFindingSeverity.High, + rule: finding.RuleID + }) + ); + }; + + return { + initialize, + postInitialization, + listRawResources, + getFullScanPath, + getDiffScanResourcePayload, + getDiffScanFindingsPayload + }; +}; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts new file mode 100644 index 000000000..830b5c5be --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts @@ -0,0 +1,87 @@ +import { z } from "zod"; + +import { + SecretScanningDataSource, + SecretScanningResource +} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; +import { + BaseCreateSecretScanningDataSourceSchema, + BaseSecretScanningDataSourceSchema, + BaseSecretScanningFindingSchema, + BaseUpdateSecretScanningDataSourceSchema, + GitRepositoryScanFindingDetailsSchema +} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas"; +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({ + includeRepos: z + .array( + z + .string() + .min(1) + .max(256) + .refine((value) => value === "*" || GitHubRepositoryRegex.test(value), "Invalid repository name format") + ) + .nonempty("One or more repositories required") + .max(100, "Cannot configure more than 100 repositories") + .default(["*"]) + .describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos) +}); + +export const BitBucketDataSourceSchema = BaseSecretScanningDataSourceSchema({ + type: SecretScanningDataSource.BitBucket, + isConnectionRequired: true +}) + .extend({ + config: BitBucketDataSourceConfigSchema + }) + .describe( + JSON.stringify({ + title: "BitBucket" + }) + ); + +export const CreateBitBucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({ + type: SecretScanningDataSource.BitBucket, + isConnectionRequired: true +}) + .extend({ + config: BitBucketDataSourceConfigSchema + }) + .describe( + JSON.stringify({ + title: "BitBucket" + }) + ); + +export const UpdateBitBucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema( + SecretScanningDataSource.BitBucket +) + .extend({ + config: BitBucketDataSourceConfigSchema.optional() + }) + .describe( + JSON.stringify({ + title: "BitBucket" + }) + ); + +export const BitBucketDataSourceListItemSchema = z + .object({ + name: z.literal("BitBucket"), + connection: z.literal(AppConnection.BitBucket), + type: z.literal(SecretScanningDataSource.BitBucket) + }) + .describe( + JSON.stringify({ + title: "BitBucket" + }) + ); + +export const BitBucketFindingSchema = BaseSecretScanningFindingSchema.extend({ + resourceType: z.literal(SecretScanningResource.Repository), + dataSourceType: z.literal(SecretScanningDataSource.BitBucket), + details: GitRepositoryScanFindingDetailsSchema +}); diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts new file mode 100644 index 000000000..508aef529 --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts @@ -0,0 +1,87 @@ +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"; + +export const bitBucketSecretScanningService = ( + secretScanningV2DAL: TSecretScanningV2DALFactory, + secretScanningV2Queue: Pick +) => { + const handleInstallationDeletedEvent = async (installationId: number) => { + const dataSource = await secretScanningV2DAL.dataSources.findOne({ + externalId: String(installationId), + type: SecretScanningDataSource.BitBucket + }); + + if (!dataSource) { + logger.error( + `secretScanningV2RemoveEvent: BitBucket - Could not find data source [installationId=${installationId}]` + ); + return; + } + + logger.info( + `secretScanningV2RemoveEvent: BitBucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]` + ); + + await secretScanningV2DAL.dataSources.updateById(dataSource.id, { + isDisconnected: true + }); + }; + + const handlePushEvent = async (payload: PushEvent) => { + const { commits, repository, installation } = payload; + + if (!commits || !repository || !installation) { + logger.warn( + `secretScanningV2PushEvent: BitBucket - Insufficient data [commits=${commits?.length ?? 0}] [repository=${repository.name}] [installationId=${installation?.id}]` + ); + return; + } + + const dataSource = (await secretScanningV2DAL.dataSources.findOne({ + externalId: String(installation.id), + type: SecretScanningDataSource.BitBucket + })) as TBitBucketDataSource | undefined; + + if (!dataSource) { + logger.error( + `secretScanningV2PushEvent: BitBucket - Could not find data source [installationId=${installation.id}]` + ); + return; + } + + const { + isAutoScanEnabled, + config: { includeRepos } + } = dataSource; + + if (!isAutoScanEnabled) { + logger.info( + `secretScanningV2PushEvent: BitBucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [installationId=${installation.id}]` + ); + return; + } + + if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) { + await secretScanningV2Queue.queueResourceDiffScan({ + 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}]` + ); + } + }; + + return { + handlePushEvent, + handleInstallationDeletedEvent + }; +}; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts new file mode 100644 index 000000000..5df73cbda --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts @@ -0,0 +1,32 @@ +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 { + BitBucketDataSourceListItemSchema, + BitBucketDataSourceSchema, + BitBucketFindingSchema, + CreateBitBucketDataSourceSchema +} from "./bitbucket-secret-scanning-schemas"; + +export type TBitBucketDataSource = z.infer; + +export type TBitBucketDataSourceInput = z.infer; + +export type TBitBucketDataSourceListItem = z.infer; + +export type TBitBucketFinding = z.infer; + +export type TBitBucketDataSourceWithConnection = TBitBucketDataSource & { + connection: TBitBucketConnection; +}; + +export type TQueueBitBucketResourceDiffScan = { + dataSourceType: SecretScanningDataSource.BitBucket; + payload: PushEvent; + dataSourceId: string; + resourceId: string; + scanId: string; +}; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/index.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/index.ts new file mode 100644 index 000000000..5ac8262f3 --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/index.ts @@ -0,0 +1,3 @@ +export * from "./bitbucket-secret-scanning-constants"; +export * from "./bitbucket-secret-scanning-schemas"; +export * from "./bitbucket-secret-scanning-types"; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts index 082f3d760..91ce3c8c4 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts @@ -1,5 +1,6 @@ export enum SecretScanningDataSource { - GitHub = "github" + GitHub = "github", + BitBucket = "bitbucket" } export enum SecretScanningScanStatus { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts index 109afe5f3..a0f7d6c65 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts @@ -1,3 +1,4 @@ +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"; @@ -15,5 +16,6 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory< >; export const SECRET_SCANNING_FACTORY_MAP: Record = { - [SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation + [SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation, + [SecretScanningDataSource.BitBucket]: BitBucketSecretScanningFactory as TSecretScanningFactoryImplementation }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 64a0ba4ed..8a3729c1c 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -4,6 +4,7 @@ import RE2 from "re2"; import { readFindingsFile } 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 { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github"; import { titleCaseToCamelCase } from "@app/lib/fn"; @@ -11,7 +12,8 @@ import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secre import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListItem } from "./secret-scanning-v2-types"; const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record = { - [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 }; export const listSecretScanningDataSourceOptions = () => { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts index f41a2b5c2..d5668b06a 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts @@ -2,13 +2,17 @@ import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/se import { AppConnection } from "@app/services/app-connection/app-connection-enums"; export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record = { - [SecretScanningDataSource.GitHub]: "GitHub" + [SecretScanningDataSource.GitHub]: "GitHub", + [SecretScanningDataSource.BitBucket]: "BitBucket" }; export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record = { - [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar + [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar, + [SecretScanningDataSource.BitBucket]: AppConnection.BitBucket }; export const AUTO_SYNC_DESCRIPTION_HELPER: Record = { - [SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" } + [SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }, + // TODO(andrey): May need change + [SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" } }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts index f1f09506f..f84254a50 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts @@ -49,6 +49,7 @@ import { TAppConnection } from "@app/services/app-connection/app-connection-type import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { bitBucketSecretScanningService } from "./bitbucket/bitbucket-secret-scanning-service"; import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal"; import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue"; @@ -869,6 +870,7 @@ export const secretScanningV2ServiceFactory = ({ updateSecretScanningFindingById, findSecretScanningConfigByProjectId, upsertSecretScanningConfig, - github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue) + github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue), + bitbucket: bitBucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue) }; }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts index 3ee5851d7..3342a8906 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts @@ -4,6 +4,14 @@ import { TSecretScanningResources, TSecretScanningScans } from "@app/db/schemas"; +import { + TBitBucketDataSource, + TBitBucketDataSourceInput, + TBitBucketDataSourceListItem, + TBitBucketDataSourceWithConnection, + TBitBucketFinding, + TQueueBitBucketResourceDiffScan +} from "@app/ee/services/secret-scanning-v2/bitbucket"; import { TGitHubDataSource, TGitHubDataSourceInput, @@ -19,7 +27,7 @@ import { SecretScanningScanStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -export type TSecretScanningDataSource = TGitHubDataSource; +export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource; export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & { lastScannedAt?: Date | null; @@ -41,13 +49,15 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & { resourceName: string; }; -export type TSecretScanningDataSourceWithConnection = TGitHubDataSourceWithConnection; +export type TSecretScanningDataSourceWithConnection = + | TGitHubDataSourceWithConnection + | TBitBucketDataSourceWithConnection; -export type TSecretScanningDataSourceInput = TGitHubDataSourceInput; +export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitBucketDataSourceInput; -export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem; +export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitBucketDataSourceListItem; -export type TSecretScanningFinding = TGitHubFinding; +export type TSecretScanningFinding = TGitHubFinding | TBitBucketFinding; export type TListSecretScanningDataSourcesByProjectId = { projectId: string; @@ -99,7 +109,7 @@ export type TQueueSecretScanningDataSourceFullScan = { scanId: string; }; -export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan; +export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitBucketResourceDiffScan; export type TQueueSecretScanningSendNotification = { dataSource: TSecretScanningDataSources; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts index 4f34791f8..b030273de 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts @@ -1,7 +1,14 @@ import { z } from "zod"; +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]); +export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [ + GitHubDataSourceSchema, + BitBucketDataSourceSchema +]); -export const SecretScanningFindingSchema = z.discriminatedUnion("resourceType", [GitHubFindingSchema]); +export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [ + GitHubFindingSchema, + BitBucketFindingSchema +]); diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx new file mode 100644 index 000000000..f215e41ca --- /dev/null +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx @@ -0,0 +1,122 @@ +import { useEffect } from "react"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { MultiValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { + TBitBucketRepo, + useBitBucketConnectionListRepositories +} from "@app/hooks/api/appConnections/bitbucket"; +import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; + +import { TSecretScanningDataSourceForm } from "../schemas"; +import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField"; + +enum ScanMethod { + AllRepositories = "all-repositories", + SelectRepositories = "select-repositories" +} + +export const BitBucketDataSourceConfigFields = () => { + const { control, watch, setValue } = useFormContext< + TSecretScanningDataSourceForm & { + type: SecretScanningDataSource.BitBucket; + } + >(); + + const connectionId = useWatch({ control, name: "connection.id" }); + const isUpdate = Boolean(watch("id")); + + const { data: repositories, isPending: areRepositoriesLoading } = + useBitBucketConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) }); + + const includeRepos = watch("config.includeRepos"); + + const scanMethod = + !includeRepos || includeRepos[0] === "*" + ? ScanMethod.AllRepositories + : ScanMethod.SelectRepositories; + + useEffect(() => { + if (!includeRepos) { + setValue("config.includeRepos", ["*"]); + } + }, [includeRepos, setValue]); + + return ( + <> + { + if (scanMethod === ScanMethod.SelectRepositories) { + setValue("config.includeRepos", []); + } + }} + /> + + + + {scanMethod === ScanMethod.SelectRepositories && ( + ( + Ensure that your connection has the correct permissions.} + > +
+ Don't see the repository you're looking for?{" "} + +
+ + } + > + value.includes(repository.name))} + onChange={(newValue) => { + onChange( + newValue ? (newValue as MultiValue).map((p) => p.name) : null + ); + }} + options={repositories} + placeholder="Select repositories..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.name} + /> +
+ )} + /> + )} + + ); +}; diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/SecretScanningDataSourceConfigFields.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/SecretScanningDataSourceConfigFields.tsx index bebcf28fc..157e614e0 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/SecretScanningDataSourceConfigFields.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/SecretScanningDataSourceConfigFields.tsx @@ -5,10 +5,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { TSecretScanningDataSourceForm } from "../schemas"; +import { BitBucketDataSourceConfigFields } from "./BitBucketDataSourceConfigFields"; import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields"; const COMPONENT_MAP: Record = { - [SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields + [SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields, + [SecretScanningDataSource.BitBucket]: BitBucketDataSourceConfigFields }; export const SecretScanningDataSourceConfigFields = () => { diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/BitBucketDataSourceReviewFields.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/BitBucketDataSourceReviewFields.tsx new file mode 100644 index 000000000..76855454a --- /dev/null +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/BitBucketDataSourceReviewFields.tsx @@ -0,0 +1,27 @@ +import { useFormContext } from "react-hook-form"; + +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; + +import { TSecretScanningDataSourceForm } from "../schemas"; +import { SecretScanningDataSourceConfigReviewSection } from "./shared"; + +export const BitBucketDataSourceReviewFields = () => { + const { watch } = useFormContext< + TSecretScanningDataSourceForm & { + type: SecretScanningDataSource.BitBucket; + } + >(); + + const [{ includeRepos }, connection] = watch(["config", "connection"]); + const shouldScanAll = includeRepos[0] === "*"; + + return ( + + {connection && {connection.name}} + + {shouldScanAll ? "All" : includeRepos.join(", ")} + + + ); +}; diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/SecretScanningDataSourceReviewFields.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/SecretScanningDataSourceReviewFields.tsx index 021b5e679..79bab1cf7 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/SecretScanningDataSourceReviewFields.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/SecretScanningDataSourceReviewFields.tsx @@ -4,10 +4,12 @@ import { GenericFieldLabel } from "@app/components/v2"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { TSecretScanningDataSourceForm } from "../schemas"; +import { BitBucketDataSourceReviewFields } from "./BitBucketDataSourceReviewFields"; import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields"; const COMPONENT_MAP: Record = { - [SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields + [SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields, + [SecretScanningDataSource.BitBucket]: BitBucketDataSourceReviewFields }; export const SecretScanningDataSourceReviewFields = () => { diff --git a/frontend/src/components/secret-scanning/forms/schemas/bitbucket-data-source-schema.ts b/frontend/src/components/secret-scanning/forms/schemas/bitbucket-data-source-schema.ts new file mode 100644 index 000000000..30c09a6fa --- /dev/null +++ b/frontend/src/components/secret-scanning/forms/schemas/bitbucket-data-source-schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; + +import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema"; + +export const BitBucketDataSourceSchema = z + .object({ + type: z.literal(SecretScanningDataSource.BitBucket), + config: z.object({ + includeRepos: z + .string() + .array() + .min(1, "One or more repositories required") + .max(100, "Cannot configure more than 100 repositories") + }) + }) + .merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true })); diff --git a/frontend/src/components/secret-scanning/forms/schemas/index.ts b/frontend/src/components/secret-scanning/forms/schemas/index.ts index bfb1ae5ec..16345affd 100644 --- a/frontend/src/components/secret-scanning/forms/schemas/index.ts +++ b/frontend/src/components/secret-scanning/forms/schemas/index.ts @@ -1,9 +1,11 @@ import { z } from "zod"; +import { BitBucketDataSourceSchema } from "./bitbucket-data-source-schema"; import { GitHubDataSourceSchema } from "./github-data-source-schema"; export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [ - GitHubDataSourceSchema + GitHubDataSourceSchema, + BitBucketDataSourceSchema ]); export type TSecretScanningDataSourceForm = z.infer; diff --git a/frontend/src/helpers/secretScanningV2.ts b/frontend/src/helpers/secretScanningV2.ts index 877a57468..fd7a12f4e 100644 --- a/frontend/src/helpers/secretScanningV2.ts +++ b/frontend/src/helpers/secretScanningV2.ts @@ -19,6 +19,11 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record< name: "GitHub", image: "GitHub.png", size: 45 + }, + [SecretScanningDataSource.BitBucket]: { + name: "BitBucket", + image: "BitBucket.png", + size: 45 } }; @@ -26,7 +31,8 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record< SecretScanningDataSource, AppConnection > = { - [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar + [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar, + [SecretScanningDataSource.BitBucket]: AppConnection.BitBucket }; export const RESOURCE_DESCRIPTION_HELPER: Record< @@ -45,6 +51,13 @@ export const RESOURCE_DESCRIPTION_HELPER: Record< singularNoun: "repository", pluralTitle: "Repositories", singularTitle: "Repository" + }, + [SecretScanningDataSource.BitBucket]: { + verb: "push", + pluralNoun: "repositories", + singularNoun: "repository", + pluralTitle: "Repositories", + singularTitle: "Repository" } }; diff --git a/frontend/src/hooks/api/secretScanningV2/enums.ts b/frontend/src/hooks/api/secretScanningV2/enums.ts index 082f3d760..91ce3c8c4 100644 --- a/frontend/src/hooks/api/secretScanningV2/enums.ts +++ b/frontend/src/hooks/api/secretScanningV2/enums.ts @@ -1,5 +1,6 @@ export enum SecretScanningDataSource { - GitHub = "github" + GitHub = "github", + BitBucket = "bitbucket" } export enum SecretScanningScanStatus { diff --git a/frontend/src/hooks/api/secretScanningV2/types/bitbucket-data-source.ts b/frontend/src/hooks/api/secretScanningV2/types/bitbucket-data-source.ts new file mode 100644 index 000000000..5de2d0fcc --- /dev/null +++ b/frontend/src/hooks/api/secretScanningV2/types/bitbucket-data-source.ts @@ -0,0 +1,17 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { SecretScanningDataSource } from "../enums"; +import { TSecretScanningDataSourceBase } from "./shared"; + +export type TBitBucketDataSource = TSecretScanningDataSourceBase & { + type: SecretScanningDataSource.BitBucket; + config: { + includeRepos: string[]; + }; +}; + +export type TBitBucketDataSourceOption = { + name: string; + type: SecretScanningDataSource.BitBucket; + connection: AppConnection.BitBucket; +}; diff --git a/frontend/src/hooks/api/secretScanningV2/types/index.ts b/frontend/src/hooks/api/secretScanningV2/types/index.ts index 07beaef5e..5621f7b36 100644 --- a/frontend/src/hooks/api/secretScanningV2/types/index.ts +++ b/frontend/src/hooks/api/secretScanningV2/types/index.ts @@ -8,9 +8,10 @@ import { SecretScanningScanStatus, SecretScanningScanType } from "../enums"; +import { TBitBucketDataSource, TBitBucketDataSourceOption } from "./bitbucket-data-source"; import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source"; -export type TSecretScanningDataSource = TGitHubDataSource; +export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource; export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & { lastScannedAt: string | null; @@ -23,7 +24,7 @@ export type TListSecretScanningDataSources = { dataSources: TSecretScanningDataSourceWithDetails[]; }; -export type TSecretScanningDataSourceOption = TGitHubDataSourceOption; +export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitBucketDataSourceOption; export type TListSecretScanningDataSourceOptions = { dataSourceOptions: TSecretScanningDataSourceOption[]; diff --git a/frontend/src/pages/project/layout.tsx b/frontend/src/pages/project/layout.tsx index f803329ca..8f85cb1a5 100644 --- a/frontend/src/pages/project/layout.tsx +++ b/frontend/src/pages/project/layout.tsx @@ -1,41 +1,37 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; -import { BreadcrumbTypes } from '@app/components/v2' -import { workspaceKeys } from '@app/hooks/api' -import { - fetchUserProjectPermissions, - roleQueryKeys, -} from '@app/hooks/api/roles/queries' -import { fetchWorkspaceById } from '@app/hooks/api/workspace/queries' -import { ProjectLayout } from '@app/layouts/ProjectLayout' -import { ProjectSelect } from '@app/layouts/ProjectLayout/components/ProjectSelect' +import { BreadcrumbTypes } from "@app/components/v2"; +import { workspaceKeys } from "@app/hooks/api"; +import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; +import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; +import { ProjectLayout } from "@app/layouts/ProjectLayout"; +import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; export const Route = createFileRoute( - '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout', + "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout" )({ component: ProjectLayout, beforeLoad: async ({ params, context }) => { const project = await context.queryClient.ensureQueryData({ queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId), - }) + queryFn: () => fetchWorkspaceById(params.projectId) + }); await context.queryClient.ensureQueryData({ queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId, + workspaceId: params.projectId }), - queryFn: () => - fetchUserProjectPermissions({ workspaceId: params.projectId }), - }) + queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + }); return { project, breadcrumbs: [ { type: BreadcrumbTypes.Component, - component: ProjectSelect, - }, - ], - } - }, -}) + component: ProjectSelect + } + ] + }; + } +}); diff --git a/frontend/src/pages/secret-manager/redirects/redirect-approval-page.tsx b/frontend/src/pages/secret-manager/redirects/redirect-approval-page.tsx index db60e8fa7..8fe2c51cf 100644 --- a/frontend/src/pages/secret-manager/redirects/redirect-approval-page.tsx +++ b/frontend/src/pages/secret-manager/redirects/redirect-approval-page.tsx @@ -1,14 +1,14 @@ -import { createFileRoute, redirect } from '@tanstack/react-router' +import { createFileRoute, redirect } from "@tanstack/react-router"; // this is done as part of migration for multi product inside project export const Route = createFileRoute( - '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval', + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval" )({ beforeLoad: ({ params, search }) => { throw redirect({ - to: '/projects/$projectId/secret-manager/approval', + to: "/projects/$projectId/secret-manager/approval", params, - search, - }) - }, -}) + search + }); + } +}); diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/BitBucketDataSourceConfigDisplay.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/BitBucketDataSourceConfigDisplay.tsx new file mode 100644 index 000000000..f23fa7e4a --- /dev/null +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/BitBucketDataSourceConfigDisplay.tsx @@ -0,0 +1,18 @@ +import { GenericFieldLabel } from "@app/components/v2"; +import { TBitBucketDataSource } from "@app/hooks/api/secretScanningV2/types/bitbucket-data-source"; + +type Props = { + dataSource: TBitBucketDataSource; +}; + +export const BitBucketDataSourceConfigDisplay = ({ dataSource }: Props) => { + const { + config: { includeRepos } + } = dataSource; + + return ( + + {includeRepos.includes("*") ? "All" : includeRepos.join(", ")} + + ); +}; diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay.tsx index 90ebd84ce..b392ddf82 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay.tsx @@ -4,6 +4,7 @@ import { } from "@app/hooks/api/secretScanningV2"; import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay"; +import { BitBucketDataSourceConfigDisplay } from "./BitBucketDataSourceConfigDisplay"; type Props = { dataSource: TSecretScanningDataSource; @@ -13,6 +14,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => { switch (dataSource.type) { case SecretScanningDataSource.GitHub: return ; + case SecretScanningDataSource.BitBucket: + return ; default: throw new Error( `Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}` From 8c6b903204ac8d67702302090b9f1c2d168c2bce Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 3 Jul 2025 02:00:14 -0400 Subject: [PATCH 04/32] Tweaks --- .../bitbucket-secret-scanning-factory.ts | 17 --------- .../secret-scanning-v2-maps.ts | 1 - .../src/server/plugins/secret-scanner-v2.ts | 2 ++ .../bitbucket/bitbucket-connection-fns.ts | 35 +++++++++++++------ .../bitbucket/bitbucket-connection-service.ts | 1 - 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts index 29ffaffaa..071133be8 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts @@ -83,7 +83,6 @@ export const BitBucketSecretScanningFactory = () => { })); }; - // TODO(andrey): Finish const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ dataSource, resourceName, @@ -131,8 +130,6 @@ export const BitBucketSecretScanningFactory = () => { } } = dataSource; - console.log("getDiffScanFindingsPayload"); - const { commits, repository } = payload; const allFindings: SecretMatch[] = []; @@ -175,16 +172,11 @@ export const BitBucketSecretScanningFactory = () => { } ); - console.log(1); - // eslint-disable-next-line no-continue if (!patch) continue; - console.log(2); // eslint-disable-next-line const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); - console.log(3); - console.log(findings); const adjustedFindings = findings.map((finding) => { const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine); @@ -195,9 +187,6 @@ export const BitBucketSecretScanningFactory = () => { const startColumn = finding.StartColumn - 1; // subtract 1 for + const endColumn = finding.EndColumn - 1; // subtract 1 for + - console.log("finding"); - console.log(finding.Link); - return { ...finding, StartLine: startLine, @@ -215,17 +204,11 @@ export const BitBucketSecretScanningFactory = () => { }; }); - console.log("adjusted"); - console.log(adjustedFindings); - allFindings.push(...adjustedFindings); } } } - console.log("HEREEE"); - console.log(allFindings); - return allFindings.map( ({ // discard match and secret as we don't want to store diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts index d5668b06a..c876a1793 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts @@ -13,6 +13,5 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record = { [SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }, - // TODO(andrey): May need change [SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" } }; diff --git a/backend/src/server/plugins/secret-scanner-v2.ts b/backend/src/server/plugins/secret-scanner-v2.ts index 466450180..6b47e324b 100644 --- a/backend/src/server/plugins/secret-scanner-v2.ts +++ b/backend/src/server/plugins/secret-scanner-v2.ts @@ -63,4 +63,6 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide return res.send("ok"); } }); + + // TODO(andrey): Register a webhook for BitBucket }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts index 11e447443..5c82365c4 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -46,16 +46,29 @@ export const validateBitBucketConnectionCredentials = async (config: TBitBucketC export const listBitBucketRepositories = async (appConnection: TBitBucketConnection) => { const { email, apiToken } = appConnection.credentials; - // TODO(andrey): Support pagination for cases where a token has access to over 100 repos - const { data } = await request.get<{ values: TBitBucketRepo[] }>( - `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`, - { - headers: { - Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, - Accept: "application/json" - } - } - ); + const headers = { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + }; - return data.values; + let allRepos: TBitBucketRepo[] = []; + let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&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 + }); + + allRepos = allRepos.concat(data.values); + nextUrl = data.next; + iterationCount += 1; + } + + return allRepos; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts index 5b9c049f7..008797823 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -16,7 +16,6 @@ export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFu const repositories = await listBitBucketRepositories(appConnection); - // TODO(andrey): May need to change from slug to ID or something return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name })); }; From 6a6f08fc4dcd1a6b730c254ca13f85fdc5d5f2cf Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 3 Jul 2025 18:49:29 -0400 Subject: [PATCH 05/32] Make webhooks work, add workspace selection, rename BitBucket to Bitbucket --- .../bitbucket-secret-scanning-router.ts | 16 +- .../v2/secret-scanning-v2-routers/index.ts | 4 +- .../secret-scanning-v2-router.ts | 4 +- .../src/ee/services/license/license-fns.ts | 56 ++-- .../bitbucket-secret-scanning-constants.ts | 6 +- .../bitbucket-secret-scanning-factory.ts | 248 +++++++++++------- .../bitbucket-secret-scanning-schemas.ts | 49 ++-- .../bitbucket-secret-scanning-service.ts | 34 +-- .../bitbucket-secret-scanning-types.ts | 83 ++++-- .../secret-scanning-v2-enums.ts | 2 +- .../secret-scanning-v2-factory.ts | 4 +- .../secret-scanning-v2-fns.ts | 2 +- .../secret-scanning-v2-maps.ts | 6 +- .../secret-scanning-v2-types.ts | 33 +-- .../secret-scanning-v2-union-schemas.ts | 6 +- backend/src/lib/api-docs/constants.ts | 5 +- .../src/server/plugins/secret-scanner-v2.ts | 30 ++- .../app-connection-router.ts | 8 +- .../bitbucket-connection-router.ts | 59 ++++- .../routes/v1/app-connection-routers/index.ts | 4 +- .../app-connection/app-connection-enums.ts | 2 +- .../app-connection/app-connection-fns.ts | 14 +- .../app-connection/app-connection-maps.ts | 4 +- .../app-connection/app-connection-service.ts | 4 +- .../app-connection/app-connection-types.ts | 16 +- .../bitbucket/bitbucket-connection-enums.ts | 2 +- .../bitbucket/bitbucket-connection-fns.ts | 79 ++++-- .../bitbucket/bitbucket-connection-schemas.ts | 50 ++-- .../bitbucket/bitbucket-connection-service.ts | 24 +- .../bitbucket/bitbucket-connection-types.ts | 29 +- .../integration-auth/integration-app-list.ts | 6 +- .../integration-auth/integration-list.ts | 2 +- .../integration-sync-secret.ts | 6 +- .../integration-auth/integration-token.ts | 20 +- docs/integrations/overview.mdx | 2 +- docs/self-hosting/configuration/envars.mdx | 4 +- .../BitBucketDataSourceConfigFields.tsx | 77 +++++- .../SecretScanningDataSourceConfigFields.tsx | 4 +- .../BitBucketDataSourceReviewFields.tsx | 4 +- .../SecretScanningDataSourceReviewFields.tsx | 4 +- .../schemas/bitbucket-data-source-schema.ts | 5 +- .../secret-scanning/forms/schemas/index.ts | 4 +- frontend/src/helpers/appConnections.ts | 6 +- frontend/src/helpers/secretScanningV2.ts | 10 +- .../api/appConnections/bitbucket/queries.tsx | 54 +++- .../api/appConnections/bitbucket/types.ts | 18 +- .../src/hooks/api/appConnections/enums.ts | 2 +- .../api/appConnections/types/app-options.ts | 8 +- .../types/bitbucket-connection.ts | 6 +- .../hooks/api/appConnections/types/index.ts | 6 +- .../src/hooks/api/integrationAuth/index.tsx | 2 +- .../src/hooks/api/integrationAuth/queries.tsx | 28 +- .../src/hooks/api/integrationAuth/types.ts | 4 +- .../src/hooks/api/secretScanningV2/enums.ts | 2 +- .../types/bitbucket-data-source.ts | 10 +- .../hooks/api/secretScanningV2/types/index.ts | 6 +- .../AppConnectionForm/AppConnectionForm.tsx | 10 +- .../BitBucketConnectionForm.tsx | 20 +- .../BitbucketConfigurePage.tsx | 8 +- .../BitBucketDataSourceConfigDisplay.tsx | 6 +- .../DataSourceConfigDisplay.tsx | 6 +- 61 files changed, 789 insertions(+), 444 deletions(-) diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts index 8aa7887e4..21fd4119b 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/bitbucket-secret-scanning-router.ts @@ -1,16 +1,16 @@ import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints"; import { - BitBucketDataSourceSchema, - CreateBitBucketDataSourceSchema, - UpdateBitBucketDataSourceSchema + BitbucketDataSourceSchema, + CreateBitbucketDataSourceSchema, + UpdateBitbucketDataSourceSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -export const registerBitBucketSecretScanningRouter = async (server: FastifyZodProvider) => +export const registerBitbucketSecretScanningRouter = async (server: FastifyZodProvider) => registerSecretScanningEndpoints({ - type: SecretScanningDataSource.BitBucket, + type: SecretScanningDataSource.Bitbucket, server, - responseSchema: BitBucketDataSourceSchema, - createSchema: CreateBitBucketDataSourceSchema, - updateSchema: UpdateBitBucketDataSourceSchema + responseSchema: BitbucketDataSourceSchema, + createSchema: CreateBitbucketDataSourceSchema, + updateSchema: UpdateBitbucketDataSourceSchema }); diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts index ff7c304f6..2258f9c82 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts @@ -1,6 +1,6 @@ import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -import { registerBitBucketSecretScanningRouter } from "./bitbucket-secret-scanning-router"; +import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router"; import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router"; export * from "./secret-scanning-v2-router"; @@ -10,5 +10,5 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record< (server: FastifyZodProvider) => Promise > = { [SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter, - [SecretScanningDataSource.BitBucket]: registerBitBucketSecretScanningRouter + [SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter }; diff --git a/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts b/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts index ed7b66a19..0a672437d 100644 --- a/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { SecretScanningConfigsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { BitBucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; +import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github"; import { SecretScanningFindingStatus, @@ -24,7 +24,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [ GitHubDataSourceListItemSchema, - BitBucketDataSourceListItemSchema + BitbucketDataSourceListItemSchema ]); export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index c2db3e6e7..dabcf2017 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -18,33 +18,33 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, - pitRecovery: false, - ipAllowlisting: false, - rbac: false, - githubOrgSync: false, + pitRecovery: true, + ipAllowlisting: true, + rbac: true, + githubOrgSync: true, customRateLimits: false, - customAlerts: false, - secretAccessInsights: false, - auditLogs: false, - auditLogsRetentionDays: 0, - auditLogStreams: false, + customAlerts: true, + secretAccessInsights: true, + auditLogs: true, + auditLogsRetentionDays: 3, + auditLogStreams: true, auditLogStreamLimit: 3, - samlSSO: false, - hsm: false, - oidcSSO: false, - scim: false, - ldap: false, - groups: false, + samlSSO: true, + hsm: true, + oidcSSO: true, + scim: true, + ldap: true, + groups: true, status: null, trial_end: null, has_used_trial: true, - secretApproval: false, - secretRotation: false, - caCrl: false, - instanceUserManagement: false, - externalKms: false, + secretApproval: true, + secretRotation: true, + caCrl: true, + instanceUserManagement: true, + externalKms: true, rateLimits: { readLimit: 60, writeLimit: 200, @@ -52,13 +52,13 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ }, pkiEst: false, enforceMfa: false, - projectTemplates: false, - kmip: false, - gateway: false, - sshHostGroups: false, - secretScanning: false, - enterpriseSecretSyncs: false, - enterpriseAppConnections: false + projectTemplates: true, + kmip: true, + gateway: true, + sshHostGroups: true, + enterpriseAppConnections: true, + enterpriseSecretSyncs: true, + secretScanning: true }); export const setupLicenseRequestWithStore = ( diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts index 0b9335915..80d22c64b 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-constants.ts @@ -3,7 +3,7 @@ import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scann import { AppConnection } from "@app/services/app-connection/app-connection-enums"; export const BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = { - name: "BitBucket", - type: SecretScanningDataSource.BitBucket, - connection: AppConnection.BitBucket + name: "Bitbucket", + type: SecretScanningDataSource.Bitbucket, + connection: AppConnection.Bitbucket }; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts index 071133be8..6840548c5 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts @@ -3,7 +3,6 @@ import { join } from "path"; import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; import { - SecretScanningDataSource, SecretScanningFindingSeverity, SecretScanningResource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; @@ -20,54 +19,106 @@ import { TSecretScanningFactoryListRawResources, TSecretScanningFactoryPostInitialization } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; import { GitHubRepositoryRegex } from "@app/lib/regex"; import { - getBitBucketUser, - listBitBucketRepositories, - TBitBucketConnection + getBitbucketUser, + listBitbucketRepositories, + TBitbucketConnection } from "@app/services/app-connection/bitbucket"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; -import { TBitBucketDataSourceWithConnection, TQueueBitBucketResourceDiffScan } from "./bitbucket-secret-scanning-types"; +import { TBitbucketDataSourceWithConnection, TQueueBitbucketResourceDiffScan } from "./bitbucket-secret-scanning-types"; -export const BitBucketSecretScanningFactory = () => { - const initialize: TSecretScanningFactoryInitialize = async ( - { connection, secretScanningV2DAL }, +export const BitbucketSecretScanningFactory = () => { + const initialize: TSecretScanningFactoryInitialize = async ( + { connection, payload }, callback ) => { - // TODO(andrey): Swap for something proper - const externalId = connection.credentials.email; + const { email, apiToken } = connection.credentials; + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; - const existingDataSource = await secretScanningV2DAL.dataSources.findOne({ - externalId, - type: SecretScanningDataSource.BitBucket - }); - - if (existingDataSource) - throw new BadRequestError({ - message: `A Data Source already exists for this BitBucket Radar Connection in the Project with ID "${existingDataSource.projectId}"` - }); + const { data } = await request.post<{ uuid: string }>( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks`, + { + description: "Infisical webhook for push events", + url: `https://tunnel.util.lol/secret-scanning/webhooks/bitbucket`, // TODO(andrey): Swap to ${cfg.SITE_URL} + active: true, + events: ["repo:push"] + }, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); return callback({ - externalId + credentials: { webhookId: data.uuid } }); }; - const postInitialization: TSecretScanningFactoryPostInitialization = async () => { - // no post-initialization required + const postInitialization: TSecretScanningFactoryPostInitialization = async ({ + dataSourceId, + credentials, + connection, + payload + }) => { + const { email, apiToken } = connection.credentials; + const { webhookId } = credentials; + + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + const cfg = getConfig(); + const newWebhookUrl = `${cfg.SITE_URL}/secret-scanning/webhooks/bitbucket?dataSourceId=${dataSourceId}`; + + await request.put( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks/${webhookId}`, + { + description: "Infisical webhook for push events", + url: newWebhookUrl, + active: true, + events: ["repo:push"] + }, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); }; - const listRawResources: TSecretScanningFactoryListRawResources = async ( + // TODO(andrey): Hook this up + const postDeletion: any = async ({ credentials, connection, payload }) => { + const { email, apiToken } = connection.credentials; + const { webhookId } = credentials; + + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + await request.delete( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${payload.config.workspaceSlug}/hooks/${webhookId}`, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); + }; + + const listRawResources: TSecretScanningFactoryListRawResources = async ( dataSource ) => { const { connection, - config: { includeRepos } + config: { includeRepos, workspaceSlug } } = dataSource; - const repos = await listBitBucketRepositories(connection); + const repos = await listBitbucketRepositories(connection, workspaceSlug); const filteredRepos: typeof repos = []; if (includeRepos.includes("*")) { @@ -83,7 +134,7 @@ export const BitBucketSecretScanningFactory = () => { })); }; - const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ + const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ dataSource, resourceName, tempFolder @@ -97,10 +148,10 @@ export const BitBucketSecretScanningFactory = () => { const repoPath = join(tempFolder, "repo.git"); if (!GitHubRepositoryRegex.test(resourceName)) { - throw new Error("Invalid BitBucket repository name"); + throw new Error("Invalid Bitbucket repository name"); } - const { username } = await getBitBucketUser({ email, apiToken }); + const { username } = await getBitbucketUser({ email, apiToken }); await cloneRepository({ cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`, @@ -111,18 +162,18 @@ export const BitBucketSecretScanningFactory = () => { }; const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload< - TQueueBitBucketResourceDiffScan["payload"] - > = ({ repository }) => { + TQueueBitbucketResourceDiffScan["payload"] + > = ({ repository, dataSourceId }) => { return { name: repository.full_name, - externalId: repository.id.toString(), + externalId: dataSourceId, type: SecretScanningResource.Repository }; }; const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload< - TBitBucketDataSourceWithConnection, - TQueueBitBucketResourceDiffScan["payload"] + TBitbucketDataSourceWithConnection, + TQueueBitbucketResourceDiffScan["payload"] > = async ({ dataSource, payload, resourceName, configPath }) => { const { connection: { @@ -130,81 +181,86 @@ export const BitBucketSecretScanningFactory = () => { } } = dataSource; - const { commits, repository } = payload; + const { push, repository } = payload; const allFindings: SecretMatch[] = []; const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; - for (const commit of commits) { - // eslint-disable-next-line no-await-in-loop - const { data: diffstat } = await request.get<{ - values: { - status: "added" | "modified" | "removed" | "renamed"; - new?: { path: string }; - old?: { path: string }; - }[]; - }>(`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diffstat/${commit.id}`, { - headers: { - Authorization: authHeader, - Accept: "application/json" - } - }); + for (const change of push.changes) { + for (const commit of change.commits) { + // eslint-disable-next-line no-await-in-loop + const { data: diffstat } = await request.get<{ + values: { + status: "added" | "modified" | "removed" | "renamed"; + new?: { path: string }; + old?: { path: string }; + }[]; + }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${repository.full_name}/diffstat/${commit.hash}`, { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + }); - // eslint-disable-next-line no-continue - if (!diffstat.values) continue; + // eslint-disable-next-line no-continue + if (!diffstat.values) continue; - for (const file of diffstat.values) { - if ((file.status === "added" || file.status === "modified") && file.new?.path) { - const filePath = file.new.path; + for (const file of diffstat.values) { + if ((file.status === "added" || file.status === "modified") && file.new?.path) { + const filePath = file.new.path; - // eslint-disable-next-line no-await-in-loop - const { data: patch } = await request.get( - `https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.id}`, - { - params: { - path: filePath - }, - headers: { - Authorization: authHeader - }, - responseType: "text" - } - ); + // eslint-disable-next-line no-await-in-loop + const { data: patch } = await request.get( + `https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.hash}`, + { + params: { + path: filePath + }, + headers: { + Authorization: authHeader + }, + responseType: "text" + } + ); - // eslint-disable-next-line no-continue - if (!patch) continue; + // eslint-disable-next-line no-continue + if (!patch) continue; - // eslint-disable-next-line - const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); + // eslint-disable-next-line no-await-in-loop + const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath); - const adjustedFindings = findings.map((finding) => { - const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine); - const endLine = - finding.StartLine === finding.EndLine - ? startLine - : convertPatchLineToFileLineNumber(patch, finding.EndLine); - const startColumn = finding.StartColumn - 1; // subtract 1 for + - const endColumn = finding.EndColumn - 1; // subtract 1 for + + const adjustedFindings = findings.map((finding) => { + const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine); + const endLine = + finding.StartLine === finding.EndLine + ? startLine + : convertPatchLineToFileLineNumber(patch, finding.EndLine); + const startColumn = finding.StartColumn - 1; // subtract 1 for + + const endColumn = finding.EndColumn - 1; // subtract 1 for + + const authorName = commit.author.user?.display_name || commit.author.raw.split(" <")[0]; + const emailMatch = commit.author.raw.match(/<(.*)>/); + const authorEmail = emailMatch?.[1] ?? ""; - return { - ...finding, - StartLine: startLine, - EndLine: endLine, - StartColumn: startColumn, - EndColumn: endColumn, - File: filePath, - Commit: commit.id, - Author: commit.author.name, - Email: commit.author.email ?? "", - Message: commit.message, - Fingerprint: `${commit.id}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`, - Date: commit.timestamp, - Link: `https://bitbucket.org/${resourceName}/src/${commit.id}/${filePath}#lines-${startLine}` - }; - }); + return { + ...finding, + StartLine: startLine, + EndLine: endLine, + StartColumn: startColumn, + EndColumn: endColumn, + File: filePath, + Commit: commit.hash, + Author: authorName, + Email: authorEmail, + Message: commit.message, + Fingerprint: `${commit.hash}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`, + Date: commit.date, + Link: `https://bitbucket.org/${resourceName}/src/${commit.hash}/${filePath}#lines-${startLine}` + }; + }); - allFindings.push(...adjustedFindings); + allFindings.push(...adjustedFindings); + } } } } diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts index 830b5c5be..a7a0d7f14 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts @@ -15,7 +15,12 @@ import { SecretScanningDataSources } from "@app/lib/api-docs"; import { GitHubRepositoryRegex } from "@app/lib/regex"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; -export const BitBucketDataSourceConfigSchema = z.object({ +export const BitbucketDataSourceConfigSchema = z.object({ + workspaceSlug: z + .string() + .min(1, "Workspace slug required") + .max(128) + .describe(SecretScanningDataSources.CONFIG.BITBUCKET.workspaceSlug), includeRepos: z .array( z @@ -30,58 +35,62 @@ export const BitBucketDataSourceConfigSchema = z.object({ .describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos) }); -export const BitBucketDataSourceSchema = BaseSecretScanningDataSourceSchema({ - type: SecretScanningDataSource.BitBucket, +export const BitbucketDataSourceSchema = BaseSecretScanningDataSourceSchema({ + type: SecretScanningDataSource.Bitbucket, isConnectionRequired: true }) .extend({ - config: BitBucketDataSourceConfigSchema + config: BitbucketDataSourceConfigSchema }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const CreateBitBucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({ - type: SecretScanningDataSource.BitBucket, +export const CreateBitbucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({ + type: SecretScanningDataSource.Bitbucket, isConnectionRequired: true }) .extend({ - config: BitBucketDataSourceConfigSchema + config: BitbucketDataSourceConfigSchema }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const UpdateBitBucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema( - SecretScanningDataSource.BitBucket +export const UpdateBitbucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema( + SecretScanningDataSource.Bitbucket ) .extend({ - config: BitBucketDataSourceConfigSchema.optional() + config: BitbucketDataSourceConfigSchema.optional() }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const BitBucketDataSourceListItemSchema = z +export const BitbucketDataSourceListItemSchema = z .object({ - name: z.literal("BitBucket"), - connection: z.literal(AppConnection.BitBucket), - type: z.literal(SecretScanningDataSource.BitBucket) + name: z.literal("Bitbucket"), + connection: z.literal(AppConnection.Bitbucket), + type: z.literal(SecretScanningDataSource.Bitbucket) }) .describe( JSON.stringify({ - title: "BitBucket" + title: "Bitbucket" }) ); -export const BitBucketFindingSchema = BaseSecretScanningFindingSchema.extend({ +export const BitbucketFindingSchema = BaseSecretScanningFindingSchema.extend({ resourceType: z.literal(SecretScanningResource.Repository), - dataSourceType: z.literal(SecretScanningDataSource.BitBucket), + dataSourceType: z.literal(SecretScanningDataSource.Bitbucket), details: GitRepositoryScanFindingDetailsSchema }); + +export const BitbucketDataSourceCredentialsSchema = z.object({ + webhookId: z.string() +}); diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts index 508aef529..ad5a59453 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts @@ -1,11 +1,9 @@ -import { PushEvent } from "@octokit/webhooks-types"; - import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue"; import { logger } from "@app/lib/logger"; -import { TBitBucketDataSource } from "./bitbucket-secret-scanning-types"; +import { TBitbucketDataSource, TBitbucketPushEvent } from "./bitbucket-secret-scanning-types"; export const bitBucketSecretScanningService = ( secretScanningV2DAL: TSecretScanningV2DALFactory, @@ -14,18 +12,18 @@ export const bitBucketSecretScanningService = ( const handleInstallationDeletedEvent = async (installationId: number) => { const dataSource = await secretScanningV2DAL.dataSources.findOne({ externalId: String(installationId), - type: SecretScanningDataSource.BitBucket + type: SecretScanningDataSource.Bitbucket }); if (!dataSource) { logger.error( - `secretScanningV2RemoveEvent: BitBucket - Could not find data source [installationId=${installationId}]` + `secretScanningV2RemoveEvent: Bitbucket - Could not find data source [installationId=${installationId}]` ); return; } logger.info( - `secretScanningV2RemoveEvent: BitBucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]` + `secretScanningV2RemoveEvent: Bitbucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]` ); await secretScanningV2DAL.dataSources.updateById(dataSource.id, { @@ -33,24 +31,26 @@ export const bitBucketSecretScanningService = ( }); }; - const handlePushEvent = async (payload: PushEvent) => { - const { commits, repository, installation } = payload; + const handlePushEvent = async (payload: TBitbucketPushEvent & { dataSourceId: string }) => { + const { push, repository } = payload; - if (!commits || !repository || !installation) { + if (!push?.changes?.length || !repository?.workspace?.uuid) { logger.warn( - `secretScanningV2PushEvent: BitBucket - Insufficient data [commits=${commits?.length ?? 0}] [repository=${repository.name}] [installationId=${installation?.id}]` + `secretScanningV2PushEvent: Bitbucket - Insufficient data [changes=${ + push?.changes?.length ?? 0 + }] [repository=${repository?.name}] [workspaceUuid=${repository?.workspace?.uuid}]` ); return; } const dataSource = (await secretScanningV2DAL.dataSources.findOne({ - externalId: String(installation.id), - type: SecretScanningDataSource.BitBucket - })) as TBitBucketDataSource | undefined; + externalId: payload.dataSourceId, + type: SecretScanningDataSource.Bitbucket + })) as TBitbucketDataSource | undefined; if (!dataSource) { logger.error( - `secretScanningV2PushEvent: BitBucket - Could not find data source [installationId=${installation.id}]` + `secretScanningV2PushEvent: Bitbucket - Could not find data source [workspaceUuid=${repository.workspace.uuid}]` ); return; } @@ -62,20 +62,20 @@ export const bitBucketSecretScanningService = ( if (!isAutoScanEnabled) { logger.info( - `secretScanningV2PushEvent: BitBucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [installationId=${installation.id}]` + `secretScanningV2PushEvent: Bitbucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [workspaceUuid=${repository.workspace.uuid}]` ); return; } if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) { await secretScanningV2Queue.queueResourceDiffScan({ - dataSourceType: SecretScanningDataSource.BitBucket, + dataSourceType: SecretScanningDataSource.Bitbucket, payload, dataSourceId: dataSource.id }); } else { logger.info( - `secretScanningV2PushEvent: BitBucket - ignoring due to repository not being present in config [installationId=${installation.id}] [dataSourceId=${dataSource.id}]` + `secretScanningV2PushEvent: Bitbucket - ignoring due to repository not being present in config [workspaceUuid=${repository.workspace.uuid}] [dataSourceId=${dataSource.id}]` ); } }; diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts index 5df73cbda..03e3f8113 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts @@ -1,31 +1,84 @@ -import { PushEvent } from "@octokit/webhooks-types"; import { z } from "zod"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -import { TBitBucketConnection } from "@app/services/app-connection/bitbucket"; +import { TBitbucketConnection } from "@app/services/app-connection/bitbucket"; import { - BitBucketDataSourceListItemSchema, - BitBucketDataSourceSchema, - BitBucketFindingSchema, - CreateBitBucketDataSourceSchema + BitbucketDataSourceCredentialsSchema, + BitbucketDataSourceListItemSchema, + BitbucketDataSourceSchema, + BitbucketFindingSchema, + CreateBitbucketDataSourceSchema } from "./bitbucket-secret-scanning-schemas"; -export type TBitBucketDataSource = z.infer; +export type TBitbucketDataSource = z.infer; -export type TBitBucketDataSourceInput = z.infer; +export type TBitbucketDataSourceInput = z.infer; -export type TBitBucketDataSourceListItem = z.infer; +export type TBitbucketDataSourceListItem = z.infer; -export type TBitBucketFinding = z.infer; +export type TBitbucketDataSourceCredentials = z.infer; -export type TBitBucketDataSourceWithConnection = TBitBucketDataSource & { - connection: TBitBucketConnection; +export type TBitbucketFinding = z.infer; + +export type TBitbucketDataSourceWithConnection = TBitbucketDataSource & { + connection: TBitbucketConnection; }; -export type TQueueBitBucketResourceDiffScan = { - dataSourceType: SecretScanningDataSource.BitBucket; - payload: PushEvent; +export type TBitbucketPushEventRepository = { + full_name: string; + name: string; + workspace: { + slug: string; + uuid: string; + }; + uuid: string; +}; + +export type TBitbucketPushEventCommit = { + hash: string; + message: string; + author: { + raw: string; + user?: { + display_name: string; + uuid: string; + nickname: string; + }; + }; + date: string; +}; + +export type TBitbucketPushEventChange = { + new?: { + name: string; + type: string; + }; + old?: { + name: string; + type: string; + }; + created: boolean; + closed: boolean; + forced: boolean; + commits: TBitbucketPushEventCommit[]; +}; + +export type TBitbucketPushEvent = { + push: { + changes: TBitbucketPushEventChange[]; + }; + repository: TBitbucketPushEventRepository; + actor: { + display_name: string; + uuid: string; + nickname: string; + }; +}; + +export type TQueueBitbucketResourceDiffScan = { + dataSourceType: SecretScanningDataSource.Bitbucket; + payload: TBitbucketPushEvent & { dataSourceId: string }; dataSourceId: string; resourceId: string; scanId: string; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts index 91ce3c8c4..40e5ea7dd 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-enums.ts @@ -1,6 +1,6 @@ export enum SecretScanningDataSource { GitHub = "github", - BitBucket = "bitbucket" + Bitbucket = "bitbucket" } export enum SecretScanningScanStatus { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts index a0f7d6c65..76e72ab77 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-factory.ts @@ -1,4 +1,4 @@ -import { BitBucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory"; +import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory"; import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory"; import { SecretScanningDataSource } from "./secret-scanning-v2-enums"; @@ -17,5 +17,5 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory< export const SECRET_SCANNING_FACTORY_MAP: Record = { [SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation, - [SecretScanningDataSource.BitBucket]: BitBucketSecretScanningFactory as TSecretScanningFactoryImplementation + [SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 8a3729c1c..9489f4658 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -13,7 +13,7 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record = { [SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION, - [SecretScanningDataSource.BitBucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION + [SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION }; export const listSecretScanningDataSourceOptions = () => { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts index c876a1793..c84d6056a 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-maps.ts @@ -3,15 +3,15 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record = { [SecretScanningDataSource.GitHub]: "GitHub", - [SecretScanningDataSource.BitBucket]: "BitBucket" + [SecretScanningDataSource.Bitbucket]: "Bitbucket" }; export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record = { [SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar, - [SecretScanningDataSource.BitBucket]: AppConnection.BitBucket + [SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket }; export const AUTO_SYNC_DESCRIPTION_HELPER: Record = { [SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }, - [SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" } + [SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" } }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts index 3342a8906..5c11a8ffb 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts @@ -5,12 +5,13 @@ import { TSecretScanningScans } from "@app/db/schemas"; import { - TBitBucketDataSource, - TBitBucketDataSourceInput, - TBitBucketDataSourceListItem, - TBitBucketDataSourceWithConnection, - TBitBucketFinding, - TQueueBitBucketResourceDiffScan + TBitbucketDataSource, + TBitbucketDataSourceCredentials, + TBitbucketDataSourceInput, + TBitbucketDataSourceListItem, + TBitbucketDataSourceWithConnection, + TBitbucketFinding, + TQueueBitbucketResourceDiffScan } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { TGitHubDataSource, @@ -27,7 +28,7 @@ import { SecretScanningScanStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; -export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource; +export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource; export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & { lastScannedAt?: Date | null; @@ -51,13 +52,15 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & { export type TSecretScanningDataSourceWithConnection = | TGitHubDataSourceWithConnection - | TBitBucketDataSourceWithConnection; + | TBitbucketDataSourceWithConnection; -export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitBucketDataSourceInput; +export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitbucketDataSourceInput; -export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitBucketDataSourceListItem; +export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitbucketDataSourceListItem; -export type TSecretScanningFinding = TGitHubFinding | TBitBucketFinding; +export type TDataSourceCredentialsSchema = TBitbucketDataSourceCredentials | undefined; + +export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding; export type TListSecretScanningDataSourcesByProjectId = { projectId: string; @@ -109,7 +112,7 @@ export type TQueueSecretScanningDataSourceFullScan = { scanId: string; }; -export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitBucketResourceDiffScan; +export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan; export type TQueueSecretScanningSendNotification = { dataSource: TSecretScanningDataSources; @@ -149,7 +152,7 @@ export type TSecretScanningDataSourceRaw = NonNullable< export type TSecretScanningFactoryInitialize< T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, - C extends TSecretScanningDataSourceCredentials = undefined + C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema > = ( params: { payload: TCreateSecretScanningDataSourceDTO; @@ -161,7 +164,7 @@ export type TSecretScanningFactoryInitialize< export type TSecretScanningFactoryPostInitialization< T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, - C extends TSecretScanningDataSourceCredentials = undefined + C extends TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema > = (params: { payload: TCreateSecretScanningDataSourceDTO; connection: T; @@ -196,4 +199,4 @@ export type TUpsertSecretScanningConfigDTO = { content: string | null; }; -export type TSecretScanningDataSourceCredentials = undefined; +export type TSecretScanningDataSourceCredentials = TDataSourceCredentialsSchema; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts index b030273de..bfac2b531 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas.ts @@ -1,14 +1,14 @@ import { z } from "zod"; -import { BitBucketDataSourceSchema, BitBucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; +import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github"; export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [ GitHubDataSourceSchema, - BitBucketDataSourceSchema + BitbucketDataSourceSchema ]); export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [ GitHubFindingSchema, - BitBucketFindingSchema + BitbucketFindingSchema ]); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e5ed91a26..145a50b23 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2270,8 +2270,8 @@ export const AppConnections = { accessTokenType: "The type of token used to connect with GitLab." }, BITBUCKET: { - email: "The email used to access BitBucket.", - apiToken: "The API token used to access BitBucket." + email: "The email used to access Bitbucket.", + apiToken: "The API token used to access Bitbucket." } } }; @@ -2634,6 +2634,7 @@ export const SecretScanningDataSources = { includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' }, BITBUCKET: { + workspaceSlug: "The workspace to scan.", includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).' } } diff --git a/backend/src/server/plugins/secret-scanner-v2.ts b/backend/src/server/plugins/secret-scanner-v2.ts index 6b47e324b..4c35adcbb 100644 --- a/backend/src/server/plugins/secret-scanner-v2.ts +++ b/backend/src/server/plugins/secret-scanner-v2.ts @@ -1,7 +1,9 @@ import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types"; import { PushEvent } from "@octokit/webhooks-types"; import { Probot } from "probot"; +import { z } from "zod"; +import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { writeLimit } from "@app/server/config/rateLimiter"; @@ -64,5 +66,31 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide } }); - // TODO(andrey): Register a webhook for BitBucket + // bitbucket push event webhook + server.route({ + method: "POST", + url: "/bitbucket", + schema: { + querystring: z.object({ + dataSourceId: z.string().min(1, { message: "Data Source ID is required" }) + }) + }, + config: { + rateLimit: writeLimit + }, + handler: async (req, res) => { + // TODO(andrey): Verify IP is from bitbucket + + const { dataSourceId } = req.query; + + if (!dataSourceId) return res.status(400).send({ message: "Data Source ID is required" }); + + await server.services.secretScanningV2.bitbucket.handlePushEvent({ + ...(req.body as TBitbucketPushEvent), + dataSourceId + }); + + return res.send("ok"); + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index be991703c..8767c0680 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -32,8 +32,8 @@ import { SanitizedAzureKeyVaultConnectionSchema } from "@app/services/app-connection/azure-key-vault"; import { - BitBucketConnectionListItemSchema, - SanitizedBitBucketConnectionSchema + BitbucketConnectionListItemSchema, + SanitizedBitbucketConnectionSchema } from "@app/services/app-connection/bitbucket"; import { CamundaConnectionListItemSchema, @@ -121,7 +121,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedFlyioConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options, - ...SanitizedBitBucketConnectionSchema.options + ...SanitizedBitbucketConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -154,7 +154,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ FlyioConnectionListItemSchema, GitLabConnectionListItemSchema, CloudflareConnectionListItemSchema, - BitBucketConnectionListItemSchema + BitbucketConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts index 4f4271273..253002e76 100644 --- a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -4,25 +4,53 @@ import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { - CreateBitBucketConnectionSchema, - SanitizedBitBucketConnectionSchema, - UpdateBitBucketConnectionSchema + CreateBitbucketConnectionSchema, + SanitizedBitbucketConnectionSchema, + UpdateBitbucketConnectionSchema } from "@app/services/app-connection/bitbucket"; import { AuthMode } from "@app/services/auth/auth-type"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; -export const registerBitBucketConnectionRouter = async (server: FastifyZodProvider) => { +export const registerBitbucketConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ - app: AppConnection.BitBucket, + app: AppConnection.Bitbucket, server, - sanitizedResponseSchema: SanitizedBitBucketConnectionSchema, - createSchema: CreateBitBucketConnectionSchema, - updateSchema: UpdateBitBucketConnectionSchema + sanitizedResponseSchema: SanitizedBitbucketConnectionSchema, + createSchema: CreateBitbucketConnectionSchema, + updateSchema: UpdateBitbucketConnectionSchema }); // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/workspaces`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + workspaces: z.object({ slug: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId } + } = req; + + const workspaces = await server.services.appConnection.bitbucket.listWorkspaces(connectionId, req.permission); + + return { workspaces }; + } + }); + server.route({ method: "GET", url: `/:connectionId/repositories`, @@ -33,17 +61,26 @@ export const registerBitBucketConnectionRouter = async (server: FastifyZodProvid params: z.object({ connectionId: z.string().uuid() }), + querystring: z.object({ + workspaceSlug: z.string() + }), response: { 200: z.object({ - repositories: z.object({ id: z.string(), name: z.string() }).array() + repositories: z.object({ slug: z.string(), full_name: z.string() }).array() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { connectionId } = req.params; + const { + params: { connectionId }, + query: { workspaceSlug } + } = req; - const repositories = await server.services.appConnection.bitbucket.listRepositories(connectionId, req.permission); + const repositories = await server.services.appConnection.bitbucket.listRepositories( + { connectionId, workspaceSlug }, + req.permission + ); return { repositories }; } diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index e164a9088..0d33afb34 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -9,7 +9,7 @@ import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-confi import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router"; import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; -import { registerBitBucketConnectionRouter } from "./bitbucket-connection-router"; +import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; @@ -64,5 +64,5 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { getFlyioConnectionListItem(), getGitLabConnectionListItem(), getCloudflareConnectionListItem(), - getBitBucketConnectionListItem() + getBitbucketConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -223,7 +223,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.BitBucket]: validateBitBucketConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -260,7 +260,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.APIToken: - case BitBucketConnectionMethod.ApiToken: + case BitbucketConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -341,7 +341,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, - [AppConnection.BitBucket]: platformManagedCredentialsNotSupported + [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index a2d953d07..cb4351fc7 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -30,7 +30,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", [AppConnection.Cloudflare]: "Cloudflare", - [AppConnection.BitBucket]: "BitBucket" + [AppConnection.Bitbucket]: "Bitbucket" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -63,5 +63,5 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -239,7 +239,7 @@ export type TAppConnectionInput = { id: string } & ( | TFlyioConnectionInput | TGitLabConnectionInput | TCloudflareConnectionInput - | TBitBucketConnectionInput + | TBitbucketConnectionInput ); export type TSqlConnectionInput = @@ -284,7 +284,7 @@ export type TAppConnectionConfig = | TFlyioConnectionConfig | TGitLabConnectionConfig | TCloudflareConnectionConfig - | TBitBucketConnectionConfig; + | TBitbucketConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -316,7 +316,7 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateFlyioConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema - | TValidateBitBucketConnectionCredentialsSchema; + | TValidateBitbucketConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts index 629c5b6d7..037915863 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-enums.ts @@ -1,3 +1,3 @@ -export enum BitBucketConnectionMethod { +export enum BitbucketConnectionMethod { ApiToken = "api-token" } diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts index 5c82365c4..14159fa49 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -5,18 +5,23 @@ import { BadRequestError } from "@app/lib/errors"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; -import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; -import { TBitBucketConnection, TBitBucketConnectionConfig, TBitBucketRepo } from "./bitbucket-connection-types"; +import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; +import { + TBitbucketConnection, + TBitbucketConnectionConfig, + TBitbucketRepo, + TBitbucketWorkspace +} from "./bitbucket-connection-types"; -export const getBitBucketConnectionListItem = () => { +export const getBitbucketConnectionListItem = () => { return { - name: "BitBucket" as const, - app: AppConnection.BitBucket as const, - methods: Object.values(BitBucketConnectionMethod) as [BitBucketConnectionMethod.ApiToken] + name: "Bitbucket" as const, + app: AppConnection.Bitbucket as const, + methods: Object.values(BitbucketConnectionMethod) as [BitbucketConnectionMethod.ApiToken] }; }; -export const getBitBucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => { +export const getBitbucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => { try { const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, { headers: { @@ -38,12 +43,17 @@ export const getBitBucketUser = async ({ email, apiToken }: { email: string; api } }; -export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => { - await getBitBucketUser(config.credentials); +export const validateBitbucketConnectionCredentials = async (config: TBitbucketConnectionConfig) => { + await getBitbucketUser(config.credentials); return config.credentials; }; -export const listBitBucketRepositories = async (appConnection: TBitBucketConnection) => { +interface BitbucketWorkspacesResponse { + values: TBitbucketWorkspace[]; + next?: string; +} + +export const listBitbucketWorkspaces = async (appConnection: TBitbucketConnection) => { const { email, apiToken } = appConnection.credentials; const headers = { @@ -51,19 +61,52 @@ export const listBitBucketRepositories = async (appConnection: TBitBucketConnect Accept: "application/json" }; - let allRepos: TBitBucketRepo[] = []; - let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`; + let allWorkspaces: TBitbucketWorkspace[] = []; + let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces?pagelen=100`; + let iterationCount = 0; + + // Limit to 10 iterations, fetching at most 10 * 100 = 1000 workspaces + while (nextUrl && iterationCount < 10) { + // eslint-disable-next-line no-await-in-loop + const { data }: { data: BitbucketWorkspacesResponse } = await request.get(nextUrl, { + headers + }); + + allWorkspaces = allWorkspaces.concat(data.values.map((workspace) => ({ slug: workspace.slug }))); + nextUrl = data.next; + iterationCount += 1; + } + + return allWorkspaces; +}; + +interface BitbucketRepositoriesResponse { + values: TBitbucketRepo[]; + next?: string; +} + +export const listBitbucketRepositories = async (appConnection: TBitbucketConnection, workspaceSlug: string) => { + const { email, apiToken } = appConnection.credentials; + + const headers = { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + }; + + let allRepos: TBitbucketRepo[] = []; + let nextUrl: string | undefined = + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}?pagelen=100`; let iterationCount = 0; // Limit to 10 iterations, fetching at most 10 * 100 = 1000 repositories while (nextUrl && iterationCount < 10) { // eslint-disable-next-line no-await-in-loop - const { data }: { data: { values: TBitBucketRepo[]; next?: string } } = await request.get<{ - values: TBitBucketRepo[]; - next?: string; - }>(nextUrl, { - headers - }); + const { data }: { data: BitbucketRepositoriesResponse } = await request.get( + nextUrl, + { + headers + } + ); allRepos = allRepos.concat(data.values); nextUrl = data.next; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts index a331db8cc..fce641c5d 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts @@ -8,54 +8,54 @@ import { GenericUpdateAppConnectionFieldsSchema } from "@app/services/app-connection/app-connection-schemas"; -import { BitBucketConnectionMethod } from "./bitbucket-connection-enums"; +import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; -export const BitBucketConnectionAccessTokenCredentialsSchema = z.object({ +export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({ apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.BITBUCKET.apiToken), email: z.string().email().trim().min(1, "Email required").describe(AppConnections.CREDENTIALS.BITBUCKET.email) }); -const BaseBitBucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.BitBucket) }); +const BaseBitbucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Bitbucket) }); -export const BitBucketConnectionSchema = BaseBitBucketConnectionSchema.extend({ - method: z.literal(BitBucketConnectionMethod.ApiToken), - credentials: BitBucketConnectionAccessTokenCredentialsSchema +export const BitbucketConnectionSchema = BaseBitbucketConnectionSchema.extend({ + method: z.literal(BitbucketConnectionMethod.ApiToken), + credentials: BitbucketConnectionAccessTokenCredentialsSchema }); -export const SanitizedBitBucketConnectionSchema = z.discriminatedUnion("method", [ - BaseBitBucketConnectionSchema.extend({ - method: z.literal(BitBucketConnectionMethod.ApiToken), - credentials: BitBucketConnectionAccessTokenCredentialsSchema.pick({ +export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method", [ + BaseBitbucketConnectionSchema.extend({ + method: z.literal(BitbucketConnectionMethod.ApiToken), + credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({ email: true }) }) ]); -export const ValidateBitBucketConnectionCredentialsSchema = z.discriminatedUnion("method", [ +export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [ z.object({ method: z - .literal(BitBucketConnectionMethod.ApiToken) - .describe(AppConnections.CREATE(AppConnection.BitBucket).method), - credentials: BitBucketConnectionAccessTokenCredentialsSchema.describe( - AppConnections.CREATE(AppConnection.BitBucket).credentials + .literal(BitbucketConnectionMethod.ApiToken) + .describe(AppConnections.CREATE(AppConnection.Bitbucket).method), + credentials: BitbucketConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Bitbucket).credentials ) }) ]); -export const CreateBitBucketConnectionSchema = ValidateBitBucketConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.BitBucket) +export const CreateBitbucketConnectionSchema = ValidateBitbucketConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Bitbucket) ); -export const UpdateBitBucketConnectionSchema = z +export const UpdateBitbucketConnectionSchema = z .object({ - credentials: BitBucketConnectionAccessTokenCredentialsSchema.optional().describe( - AppConnections.UPDATE(AppConnection.BitBucket).credentials + credentials: BitbucketConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Bitbucket).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.BitBucket)); + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket)); -export const BitBucketConnectionListItemSchema = z.object({ - name: z.literal("BitBucket"), - app: z.literal(AppConnection.BitBucket), - methods: z.nativeEnum(BitBucketConnectionMethod).array() +export const BitbucketConnectionListItemSchema = z.object({ + name: z.literal("Bitbucket"), + app: z.literal(AppConnection.Bitbucket), + methods: z.nativeEnum(BitbucketConnectionMethod).array() }); diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts index 008797823..290ac21e3 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -1,25 +1,33 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { listBitBucketRepositories } from "./bitbucket-connection-fns"; -import { TBitBucketConnection } from "./bitbucket-connection-types"; +import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns"; +import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, connectionId: string, actor: OrgServiceActor -) => Promise; +) => Promise; export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => { - const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { - const appConnection = await getAppConnection(AppConnection.BitBucket, connectionId, actor); + const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const workspaces = await listBitbucketWorkspaces(appConnection); + return workspaces; + }; - const repositories = await listBitBucketRepositories(appConnection); - - return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name })); + const listRepositories = async ( + { connectionId, workspaceSlug }: TGetBitbucketRepositoriesDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const repositories = await listBitbucketRepositories(appConnection, workspaceSlug); + return repositories; }; return { + listWorkspaces, listRepositories }; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts index 41fdaef29..53e93b479 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -4,27 +4,36 @@ import { DiscriminativePick } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; import { - BitBucketConnectionSchema, - CreateBitBucketConnectionSchema, - ValidateBitBucketConnectionCredentialsSchema + BitbucketConnectionSchema, + CreateBitbucketConnectionSchema, + ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket-connection-schemas"; -export type TBitBucketConnection = z.infer; +export type TBitbucketConnection = z.infer; -export type TBitBucketConnectionInput = z.infer & { - app: AppConnection.BitBucket; +export type TBitbucketConnectionInput = z.infer & { + app: AppConnection.Bitbucket; }; -export type TValidateBitBucketConnectionCredentialsSchema = typeof ValidateBitBucketConnectionCredentialsSchema; +export type TValidateBitbucketConnectionCredentialsSchema = typeof ValidateBitbucketConnectionCredentialsSchema; -export type TBitBucketConnectionConfig = DiscriminativePick< - TBitBucketConnectionInput, +export type TBitbucketConnectionConfig = DiscriminativePick< + TBitbucketConnectionInput, "method" | "app" | "credentials" > & { orgId: string; }; -export type TBitBucketRepo = { +export type TGetBitbucketRepositoriesDTO = { + connectionId: string; + workspaceSlug: string; +}; + +export type TBitbucketWorkspace = { + slug: string; +}; + +export type TBitbucketRepo = { full_name: string; // workspace-slug/repo-slug slug: string; }; diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 4a5fd8231..c549d1265 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -814,9 +814,9 @@ const getAppsCloudflareWorkers = async ({ accessToken, accountId }: { accessToke }; /** - * Return list of repositories for the BitBucket integration based on provided BitBucket workspace + * Return list of repositories for the Bitbucket integration based on provided Bitbucket workspace */ -const getAppsBitBucket = async ({ accessToken, workspaceSlug }: { accessToken: string; workspaceSlug?: string }) => { +const getAppsBitbucket = async ({ accessToken, workspaceSlug }: { accessToken: string; workspaceSlug?: string }) => { interface RepositoriesResponse { size: number; page: number; @@ -1302,7 +1302,7 @@ export const getApps = async ({ }); case Integrations.BITBUCKET: - return getAppsBitBucket({ + return getAppsBitbucket({ accessToken, workspaceSlug }); diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 9b9841f1f..0608bbd4b 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -342,7 +342,7 @@ export const getIntegrationOptions = async () => { { name: "Bitbucket", slug: "bitbucket", - image: "BitBucket.png", + image: "Bitbucket.png", isAvailable: true, type: "oauth", clientId: appCfg.CLIENT_ID_BITBUCKET, diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 989a5a88c..1cd4569ac 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -3921,9 +3921,9 @@ const syncSecretsCloudflareWorkers = async ({ }; /** - * Sync/push [secrets] to BitBucket repo with name [integration.app] + * Sync/push [secrets] to Bitbucket repo with name [integration.app] */ -const syncSecretsBitBucket = async ({ +const syncSecretsBitbucket = async ({ integration, secrets, accessToken @@ -4832,7 +4832,7 @@ export const syncIntegrationSecrets = async ({ }); break; case Integrations.BITBUCKET: - await syncSecretsBitBucket({ + await syncSecretsBitbucket({ integration, secrets, accessToken diff --git a/backend/src/services/integration-auth/integration-token.ts b/backend/src/services/integration-auth/integration-token.ts index 362b20a07..a15c9dd1f 100644 --- a/backend/src/services/integration-auth/integration-token.ts +++ b/backend/src/services/integration-auth/integration-token.ts @@ -64,7 +64,7 @@ type ExchangeCodeGitlabResponse = { created_at: number; }; -type ExchangeCodeBitBucketResponse = { +type ExchangeCodeBitbucketResponse = { access_token: string; token_type: string; expires_in: number; @@ -392,10 +392,10 @@ const exchangeCodeGitlab = async ({ code, url }: { code: string; url?: string }) }; /** - * Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Bitbucket * code-token exchange */ -const exchangeCodeBitBucket = async ({ code }: { code: string }) => { +const exchangeCodeBitbucket = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); const appCfg = getConfig(); if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) { @@ -403,7 +403,7 @@ const exchangeCodeBitBucket = async ({ code }: { code: string }) => { } const res = ( - await request.post( + await request.post( IntegrationUrls.BITBUCKET_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", @@ -490,7 +490,7 @@ export const exchangeCode = async ({ url }); case Integrations.BITBUCKET: - return exchangeCodeBitBucket({ + return exchangeCodeBitbucket({ code }); default: @@ -524,7 +524,7 @@ type RefreshTokenGitLabResponse = { created_at: number; }; -type RefreshTokenBitBucketResponse = { +type RefreshTokenBitbucketResponse = { access_token: string; token_type: string; expires_in: number; @@ -653,9 +653,9 @@ const exchangeRefreshGitLab = async ({ refreshToken, url }: { url?: string | nul /** * Return new access token by exchanging refresh token [refreshToken] for the - * BitBucket integration + * Bitbucket integration */ -const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string }) => { +const exchangeRefreshBitbucket = async ({ refreshToken }: { refreshToken: string }) => { const accessExpiresAt = new Date(); const appCfg = getConfig(); if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) { @@ -664,7 +664,7 @@ const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string const { data }: { - data: RefreshTokenBitBucketResponse; + data: RefreshTokenBitbucketResponse; } = await request.post( IntegrationUrls.BITBUCKET_TOKEN_URL, new URLSearchParams({ @@ -794,7 +794,7 @@ export const exchangeRefresh = async ( url }); case Integrations.BITBUCKET: - return exchangeRefreshBitBucket({ + return exchangeRefreshBitbucket({ refreshToken }); case Integrations.GCP_SECRET_MANAGER: diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 08debf8cf..5dc06daed 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -35,7 +35,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | | [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | | [Windmill](/integrations/cloud/windmill) | Cloud | Available | -| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available | +| [Bitbucket](/integrations/cicd/bitbucket) | CI/CD | Available | | [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available | | [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | | [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 90f3b2207..29f5d8f15 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -669,11 +669,11 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I - OAuth2 client ID for BitBucket integration + OAuth2 client ID for Bitbucket integration - OAuth2 client secret for BitBucket integration + OAuth2 client secret for Bitbucket integration diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx index f215e41ca..98369f2c4 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitBucketDataSourceConfigFields.tsx @@ -1,13 +1,15 @@ import { useEffect } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -import { MultiValue } from "react-select"; +import { MultiValue, SingleValue } from "react-select"; import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; import { - TBitBucketRepo, - useBitBucketConnectionListRepositories + TBitbucketRepo, + TBitbucketWorkspace, + useBitbucketConnectionListRepositories, + useBitbucketConnectionListWorkspaces } from "@app/hooks/api/appConnections/bitbucket"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; @@ -19,18 +21,25 @@ enum ScanMethod { SelectRepositories = "select-repositories" } -export const BitBucketDataSourceConfigFields = () => { +export const BitbucketDataSourceConfigFields = () => { const { control, watch, setValue } = useFormContext< TSecretScanningDataSourceForm & { - type: SecretScanningDataSource.BitBucket; + type: SecretScanningDataSource.Bitbucket; } >(); const connectionId = useWatch({ control, name: "connection.id" }); const isUpdate = Boolean(watch("id")); + const selectedWorkspaceSlug = useWatch({ control, name: "config.workspaceSlug" }); + + const { data: workspaces, isPending: areWorkspacesLoading } = + useBitbucketConnectionListWorkspaces(connectionId, { enabled: Boolean(connectionId) }); + const { data: repositories, isPending: areRepositoriesLoading } = - useBitBucketConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) }); + useBitbucketConnectionListRepositories(connectionId, selectedWorkspaceSlug, { + enabled: Boolean(connectionId) && Boolean(selectedWorkspaceSlug) // Enable only if both are present + }); const includeRepos = watch("config.includeRepos"); @@ -51,10 +60,50 @@ export const BitBucketDataSourceConfigFields = () => { isUpdate={isUpdate} onChange={() => { if (scanMethod === ScanMethod.SelectRepositories) { + setValue("config.workspaceSlug", ""); setValue("config.includeRepos", []); } }} /> + ( + Ensure that your connection has the correct permissions.} + > +
+ Don't see the workspaces you're looking for?{" "} + +
+ + } + > + { + onChange((newValue as SingleValue)?.slug); + if (scanMethod === ScanMethod.SelectRepositories) { + setValue("config.includeRepos", []); + } + }} + options={workspaces} + placeholder="Select workspace..." + getOptionLabel={(option) => option.slug} + getOptionValue={(option) => option.slug} + /> +
+ )} + />