diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index c799aaf23..278fca695 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -19,7 +19,7 @@ WORKDIR /app # Copy dependencies COPY --from=frontend-dependencies /app/node_modules ./node_modules -# Copy all files +# Copy all files COPY /frontend . ENV NODE_ENV production @@ -32,7 +32,7 @@ ENV VITE_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY -ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY +ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY # Build RUN npm run build @@ -134,7 +134,7 @@ RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-li # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.41.2 \ + && apt-get update && apt-get install -y infisical=0.41.89 \ && rm -rf /var/lib/apt/lists/* RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user @@ -155,7 +155,7 @@ ENV INTERCOM_ID=$INTERCOM_ID ARG CAPTCHA_SITE_KEY ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY -WORKDIR / +WORKDIR / COPY --from=backend-runner /app /backend @@ -166,9 +166,9 @@ ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV PORT 8080 ENV HOST=0.0.0.0 -ENV HTTPS_ENABLED false +ENV HTTPS_ENABLED false ENV NODE_ENV production -ENV STANDALONE_BUILD true +ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ ENV NODE_OPTIONS="--max-old-space-size=1024" diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 45295dec8..01113f019 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -20,7 +20,7 @@ WORKDIR /app # Copy dependencies COPY --from=frontend-dependencies /app/node_modules ./node_modules -# Copy all files +# Copy all files COPY /frontend . ENV NODE_ENV production @@ -33,7 +33,7 @@ ENV VITE_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY -ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY +ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY # Build RUN npm run build @@ -128,7 +128,7 @@ RUN apt-get update && apt-get install -y \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.41.2 \ + && apt-get update && apt-get install -y infisical=0.41.89 \ && rm -rf /var/lib/apt/lists/* WORKDIR / @@ -164,9 +164,9 @@ ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV PORT 8080 ENV HOST=0.0.0.0 -ENV HTTPS_ENABLED false +ENV HTTPS_ENABLED false ENV NODE_ENV production -ENV STANDALONE_BUILD true +ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV NODE_OPTIONS="--max-old-space-size=1024" diff --git a/backend/Dockerfile b/backend/Dockerfile index 79333cb92..bca974f26 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y \ make \ g++ \ openssh-client \ - openssl + openssl # Install dependencies for TDS driver (required for SAP ASE dynamic secrets) RUN apt-get install -y \ @@ -55,10 +55,10 @@ COPY --from=build /app . # Install Infisical CLI RUN apt-get install -y curl bash && \ curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ - apt-get update && apt-get install -y infisical=0.41.2 git + apt-get update && apt-get install -y infisical=0.41.89 git -HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ - CMD node healthcheck.js +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node healthcheck.js ENV HOST=0.0.0.0 diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 75c561ac1..de5648797 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -57,7 +57,7 @@ RUN mkdir -p /etc/softhsm2/tokens && \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ apt-get update && \ - apt-get install -y infisical=0.41.2 + apt-get install -y infisical=0.41.89 WORKDIR /app diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index 0afb330e5..8513c982b 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -52,7 +52,7 @@ RUN apt-get install -y opensc RUN mkdir -p /etc/softhsm2/tokens && \ softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - + WORKDIR /openssl-build RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && tar -xf openssl-3.1.2.tar.gz \ @@ -66,7 +66,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ apt-get update && \ - apt-get install -y infisical=0.41.2 + apt-get install -y infisical=0.41.89 WORKDIR /app 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..21fd4119b --- /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..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,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..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,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..80d22c64b --- /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..f51c75daa --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts @@ -0,0 +1,314 @@ +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 { + 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, + TSecretScanningFactoryTeardown +} 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 { titleCaseToCamelCase } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { BasicRepositoryRegex } from "@app/lib/regex"; +import { + getBitbucketUser, + listBitbucketRepositories, + TBitbucketConnection +} from "@app/services/app-connection/bitbucket"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { + TBitbucketDataSourceCredentials, + TBitbucketDataSourceInput, + TBitbucketDataSourceWithConnection, + TQueueBitbucketResourceDiffScan +} from "./bitbucket-secret-scanning-types"; + +export const BitbucketSecretScanningFactory = () => { + const initialize: TSecretScanningFactoryInitialize< + TBitbucketDataSourceInput, + TBitbucketConnection, + TBitbucketDataSourceCredentials + > = async ({ connection, payload }, callback) => { + const cfg = getConfig(); + + const { email, apiToken } = connection.credentials; + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + const { data } = await request.post<{ uuid: string }>( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${encodeURIComponent(payload.config.workspaceSlug)}/hooks`, + { + description: "Infisical webhook for push events", + url: `${cfg.SITE_URL}/secret-scanning/webhooks/bitbucket`, + active: false, + events: ["repo:push"] + }, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); + + return callback({ + credentials: { webhookId: data.uuid, webhookSecret: alphaNumericNanoId(64) } + }); + }; + + const postInitialization: TSecretScanningFactoryPostInitialization< + TBitbucketDataSourceInput, + TBitbucketConnection, + TBitbucketDataSourceCredentials + > = async ({ dataSourceId, credentials, connection, payload }) => { + const { email, apiToken } = connection.credentials; + const { webhookId, webhookSecret } = 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/${encodeURIComponent(payload.config.workspaceSlug)}/hooks/${webhookId}`, + { + description: "Infisical webhook for push events", + url: newWebhookUrl, + active: true, + events: ["repo:push"], + secret: webhookSecret + }, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); + }; + + const teardown: TSecretScanningFactoryTeardown< + TBitbucketDataSourceWithConnection, + TBitbucketDataSourceCredentials + > = async ({ credentials, dataSource }) => { + const { + connection: { + credentials: { email, apiToken } + }, + config + } = dataSource; + const { webhookId } = credentials; + + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + try { + await request.delete( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces/${config.workspaceSlug}/hooks/${webhookId}`, + { + headers: { + Authorization: authHeader, + Accept: "application/json" + } + } + ); + } catch (err) { + logger.error(`teardown: Bitbucket - Failed to call delete on webhook [webhookId=${webhookId}]`); + } + }; + + const listRawResources: TSecretScanningFactoryListRawResources = async ( + dataSource + ) => { + const { + connection, + config: { includeRepos, workspaceSlug } + } = dataSource; + + const repos = await listBitbucketRepositories(connection, workspaceSlug); + + const filteredRepos: typeof repos = []; + if (includeRepos.includes("*")) { + filteredRepos.push(...repos); + } else { + filteredRepos.push(...repos.filter((repo) => includeRepos.includes(repo.full_name))); + } + + return filteredRepos.map(({ full_name, uuid }) => ({ + name: full_name, + externalId: uuid, + type: SecretScanningResource.Repository + })); + }; + + const getFullScanPath: TSecretScanningFactoryGetFullScanPath = async ({ + dataSource, + resourceName, + tempFolder + }) => { + const { + connection: { + credentials: { apiToken, email } + } + } = dataSource; + + const repoPath = join(tempFolder, "repo.git"); + + if (!BasicRepositoryRegex.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.uuid, + type: SecretScanningResource.Repository + }; + }; + + const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload< + TBitbucketDataSourceWithConnection, + TQueueBitbucketResourceDiffScan["payload"] + > = async ({ dataSource, payload, resourceName, configPath }) => { + const { + connection: { + credentials: { apiToken, email } + } + } = dataSource; + + const { push, repository } = payload; + + const allFindings: SecretMatch[] = []; + + const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + 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; + + 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.hash}`, + { + params: { + path: filePath + }, + headers: { + Authorization: authHeader + }, + responseType: "text" + } + ); + + // eslint-disable-next-line no-continue + if (!patch) continue; + + // 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 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.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); + } + } + } + } + + 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, + teardown + }; +}; 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..36d036f15 --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-schemas.ts @@ -0,0 +1,97 @@ +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 { BasicRepositoryRegex } from "@app/lib/regex"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const BitbucketDataSourceConfigSchema = z.object({ + workspaceSlug: z + .string() + .min(1, "Workspace slug required") + .max(128) + .describe(SecretScanningDataSources.CONFIG.BITBUCKET.workspaceSlug), + includeRepos: z + .array( + z + .string() + .min(1) + .max(256) + .refine((value) => value === "*" || BasicRepositoryRegex.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 +}); + +export const BitbucketDataSourceCredentialsSchema = z.object({ + webhookId: z.string(), + webhookSecret: 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 new file mode 100644 index 000000000..c5a0aedd6 --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts @@ -0,0 +1,104 @@ +import crypto from "crypto"; + +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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { + TBitbucketDataSource, + TBitbucketDataSourceCredentials, + TBitbucketPushEvent +} from "./bitbucket-secret-scanning-types"; + +export const bitbucketSecretScanningService = ( + secretScanningV2DAL: TSecretScanningV2DALFactory, + secretScanningV2Queue: Pick, + kmsService: Pick +) => { + const handlePushEvent = async ( + payload: TBitbucketPushEvent & { dataSourceId: string; receivedSignature: string; bodyString: string } + ) => { + const { push, repository, bodyString, receivedSignature } = payload; + + if (!push?.changes?.length || !repository?.workspace?.uuid) { + logger.warn( + `secretScanningV2PushEvent: Bitbucket - Insufficient data [changes=${ + push?.changes?.length ?? 0 + }] [repository=${repository?.name}] [workspaceUuid=${repository?.workspace?.uuid}]` + ); + return; + } + + const dataSource = (await secretScanningV2DAL.dataSources.findOne({ + id: payload.dataSourceId, + type: SecretScanningDataSource.Bitbucket + })) as TBitbucketDataSource | undefined; + + if (!dataSource) { + logger.error( + `secretScanningV2PushEvent: Bitbucket - Could not find data source [workspaceUuid=${repository.workspace.uuid}]` + ); + return; + } + + const { + isAutoScanEnabled, + config: { includeRepos }, + encryptedCredentials, + projectId + } = dataSource; + + if (!encryptedCredentials) { + logger.info( + `secretScanningV2PushEvent: Bitbucket - Could not find encrypted credentials [dataSourceId=${dataSource.id}] [workspaceUuid=${repository.workspace.uuid}]` + ); + return; + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedCredentials = decryptor({ cipherTextBlob: encryptedCredentials }); + + const credentials = JSON.parse(decryptedCredentials.toString()) as TBitbucketDataSourceCredentials; + + const hmac = crypto.createHmac("sha256", credentials.webhookSecret); + hmac.update(bodyString); + const calculatedSignature = hmac.digest("hex"); + + if (calculatedSignature !== receivedSignature) { + logger.error( + `secretScanningV2PushEvent: Bitbucket - Invalid signature for webhook [dataSourceId=${dataSource.id}] [workspaceUuid=${repository.workspace.uuid}]` + ); + return; + } + + if (!isAutoScanEnabled) { + logger.info( + `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, + payload, + dataSourceId: dataSource.id + }); + } else { + logger.info( + `secretScanningV2PushEvent: Bitbucket - ignoring due to repository not being present in config [workspaceUuid=${repository.workspace.uuid}] [dataSourceId=${dataSource.id}]` + ); + } + }; + + return { + handlePushEvent + }; +}; 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..03e3f8113 --- /dev/null +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types.ts @@ -0,0 +1,85 @@ +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 { + BitbucketDataSourceCredentialsSchema, + 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 TBitbucketDataSourceCredentials = z.infer; + +export type TBitbucketFinding = z.infer; + +export type TBitbucketDataSourceWithConnection = TBitbucketDataSource & { + connection: TBitbucketConnection; +}; + +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/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/github/github-secret-scanning-factory.ts b/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-factory.ts index 2dde97d7c..e09b8f88b 100644 --- a/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-factory.ts @@ -19,18 +19,23 @@ import { TSecretScanningFactoryGetFullScanPath, TSecretScanningFactoryInitialize, TSecretScanningFactoryListRawResources, - TSecretScanningFactoryPostInitialization + TSecretScanningFactoryPostInitialization, + TSecretScanningFactoryTeardown } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; -import { GitHubRepositoryRegex } from "@app/lib/regex"; +import { BasicRepositoryRegex } from "@app/lib/regex"; import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/services/app-connection/github-radar"; -import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types"; +import { + TGitHubDataSourceInput, + TGitHubDataSourceWithConnection, + TQueueGitHubResourceDiffScan +} from "./github-secret-scanning-types"; export const GitHubSecretScanningFactory = () => { - const initialize: TSecretScanningFactoryInitialize = async ( + const initialize: TSecretScanningFactoryInitialize = async ( { connection, secretScanningV2DAL }, callback ) => { @@ -51,10 +56,17 @@ export const GitHubSecretScanningFactory = () => { }); }; - const postInitialization: TSecretScanningFactoryPostInitialization = async () => { + const postInitialization: TSecretScanningFactoryPostInitialization< + TGitHubDataSourceInput, + TGitHubRadarConnection + > = async () => { // no post-initialization required }; + const teardown: TSecretScanningFactoryTeardown = async () => { + // no termination required + }; + const listRawResources: TSecretScanningFactoryListRawResources = async ( dataSource ) => { @@ -107,7 +119,7 @@ export const GitHubSecretScanningFactory = () => { const repoPath = join(tempFolder, "repo.git"); - if (!GitHubRepositoryRegex.test(resourceName)) { + if (!BasicRepositoryRegex.test(resourceName)) { throw new Error("Invalid GitHub repository name"); } @@ -225,6 +237,7 @@ export const GitHubSecretScanningFactory = () => { listRawResources, getFullScanPath, getDiffScanResourcePayload, - getDiffScanFindingsPayload + getDiffScanFindingsPayload, + teardown }; }; diff --git a/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-schemas.ts b/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-schemas.ts index f1eec125c..e39830f22 100644 --- a/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/github/github-secret-scanning-schemas.ts @@ -12,7 +12,7 @@ import { 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 { BasicRepositoryRegex } from "@app/lib/regex"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; export const GitHubDataSourceConfigSchema = z.object({ @@ -22,7 +22,7 @@ export const GitHubDataSourceConfigSchema = z.object({ .string() .min(1) .max(256) - .refine((value) => value === "*" || GitHubRepositoryRegex.test(value), "Invalid repository name format") + .refine((value) => value === "*" || BasicRepositoryRegex.test(value), "Invalid repository name format") ) .nonempty("One or more repositories required") .max(100, "Cannot configure more than 100 repositories") 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..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,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..2ca1f4882 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,19 +1,23 @@ +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"; import { TQueueSecretScanningResourceDiffScan, TSecretScanningDataSourceCredentials, + TSecretScanningDataSourceInput, TSecretScanningDataSourceWithConnection, TSecretScanningFactory } from "./secret-scanning-v2-types"; type TSecretScanningFactoryImplementation = TSecretScanningFactory< TSecretScanningDataSourceWithConnection, - TSecretScanningDataSourceCredentials, - TQueueSecretScanningResourceDiffScan["payload"] + TQueueSecretScanningResourceDiffScan["payload"], + TSecretScanningDataSourceInput, + TSecretScanningDataSourceCredentials >; 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..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 @@ -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..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 @@ -2,13 +2,16 @@ 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" }, + [SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" } }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-schemas.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-schemas.ts index 832b73bda..730427a18 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-schemas.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-schemas.ts @@ -19,8 +19,7 @@ export const BaseSecretScanningDataSourceSchema = ({ // unique to provider type: true, connectionId: true, - config: true, - encryptedCredentials: true + config: true }).extend({ type: z.literal(type), connectionId: isConnectionRequired ? z.string().uuid() : z.null(), 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..761059d2a 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 @@ -30,6 +30,8 @@ import { TFindSecretScanningDataSourceByNameDTO, TListSecretScanningDataSourcesByProjectId, TSecretScanningDataSource, + TSecretScanningDataSourceCredentials, + TSecretScanningDataSourceInput, TSecretScanningDataSourceWithConnection, TSecretScanningDataSourceWithDetails, TSecretScanningFinding, @@ -49,6 +51,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"; @@ -256,7 +259,7 @@ export const secretScanningV2ServiceFactory = ({ try { const createdDataSource = await factory.initialize( { - payload, + payload: payload as TSecretScanningDataSourceInput, connection: connection as TSecretScanningDataSourceWithConnection["connection"], secretScanningV2DAL }, @@ -287,7 +290,7 @@ export const secretScanningV2ServiceFactory = ({ ); await factory.postInitialization({ - payload, + payload: payload as TSecretScanningDataSourceInput, connection: connection as TSecretScanningDataSourceWithConnection["connection"], dataSourceId: dataSource.id, credentials @@ -398,7 +401,6 @@ export const secretScanningV2ServiceFactory = ({ actorId: actor.id, actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, - projectId: dataSource.projectId }); @@ -412,7 +414,36 @@ export const secretScanningV2ServiceFactory = ({ message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}` }); - // TODO: clean up webhooks + const factory = SECRET_SCANNING_FACTORY_MAP[type](); + + let connection: TAppConnection | null = null; + if (dataSource.connection) { + connection = await decryptAppConnection(dataSource.connection, kmsService); + } + + let credentials: TSecretScanningDataSourceCredentials | undefined; + + if (dataSource.encryptedCredentials) { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: dataSource.projectId + }); + + credentials = JSON.parse( + decryptor({ + cipherTextBlob: dataSource.encryptedCredentials + }).toString() + ) as TSecretScanningDataSourceCredentials; + } + + await factory.teardown({ + dataSource: { + ...dataSource, + // @ts-expect-error currently we don't have a null connection data source + connection + }, + credentials + }); await secretScanningV2DAL.dataSources.deleteById(dataSourceId); @@ -869,6 +900,7 @@ export const secretScanningV2ServiceFactory = ({ updateSecretScanningFindingById, findSecretScanningConfigByProjectId, upsertSecretScanningConfig, - github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue) + github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue), + bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService) }; }; 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..35486bd59 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,15 @@ import { TSecretScanningResources, TSecretScanningScans } from "@app/db/schemas"; +import { + TBitbucketDataSource, + TBitbucketDataSourceCredentials, + TBitbucketDataSourceInput, + TBitbucketDataSourceListItem, + TBitbucketDataSourceWithConnection, + TBitbucketFinding, + TQueueBitbucketResourceDiffScan +} from "@app/ee/services/secret-scanning-v2/bitbucket"; import { TGitHubDataSource, TGitHubDataSourceInput, @@ -19,7 +28,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 +50,17 @@ 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 TSecretScanningDataSourceCredentials = TBitbucketDataSourceCredentials | undefined; + +export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding; export type TListSecretScanningDataSourcesByProjectId = { projectId: string; @@ -99,7 +112,7 @@ export type TQueueSecretScanningDataSourceFullScan = { scanId: string; }; -export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan; +export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan; export type TQueueSecretScanningSendNotification = { dataSource: TSecretScanningDataSources; @@ -138,11 +151,12 @@ export type TSecretScanningDataSourceRaw = NonNullable< >; export type TSecretScanningFactoryInitialize< + P extends TSecretScanningDataSourceInput, T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, C extends TSecretScanningDataSourceCredentials = undefined > = ( params: { - payload: TCreateSecretScanningDataSourceDTO; + payload: P; connection: T; secretScanningV2DAL: TSecretScanningV2DALFactory; }, @@ -150,24 +164,27 @@ export type TSecretScanningFactoryInitialize< ) => Promise; export type TSecretScanningFactoryPostInitialization< + P extends TSecretScanningDataSourceInput, T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined, C extends TSecretScanningDataSourceCredentials = undefined -> = (params: { - payload: TCreateSecretScanningDataSourceDTO; - connection: T; - credentials: C; - dataSourceId: string; -}) => Promise; +> = (params: { payload: P; connection: T; credentials: C; dataSourceId: string }) => Promise; + +export type TSecretScanningFactoryTeardown< + T extends TSecretScanningDataSourceWithConnection, + C extends TSecretScanningDataSourceCredentials = undefined +> = (params: { dataSource: T; credentials: C }) => Promise; export type TSecretScanningFactory< T extends TSecretScanningDataSourceWithConnection, - C extends TSecretScanningDataSourceCredentials, - P extends TQueueSecretScanningResourceDiffScan["payload"] + P extends TQueueSecretScanningResourceDiffScan["payload"], + I extends TSecretScanningDataSourceInput, + C extends TSecretScanningDataSourceCredentials | undefined = undefined > = () => { listRawResources: TSecretScanningFactoryListRawResources; getFullScanPath: TSecretScanningFactoryGetFullScanPath; - initialize: TSecretScanningFactoryInitialize; - postInitialization: TSecretScanningFactoryPostInitialization; + initialize: TSecretScanningFactoryInitialize; + postInitialization: TSecretScanningFactoryPostInitialization; + teardown: TSecretScanningFactoryTeardown; getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload

; getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload; }; @@ -185,5 +202,3 @@ export type TUpsertSecretScanningConfigDTO = { projectId: string; content: string | null; }; - -export type TSecretScanningDataSourceCredentials = undefined; 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..671d4e16b 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,22 @@ 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.describe( + JSON.stringify({ + title: "GitHub" + }) + ), + BitbucketFindingSchema.describe( + JSON.stringify({ + title: "Bitbucket" + }) + ) +]); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f3d73896e..de5060f6f 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2272,6 +2272,10 @@ export const AppConnections = { 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." + }, ZABBIX: { apiToken: "The API Token used to access Zabbix.", instanceUrl: "The Zabbix instance URL to connect with." @@ -2641,6 +2645,10 @@ export const SecretScanningDataSources = { CONFIG: { GITHUB: { 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/lib/regex/index.ts b/backend/src/lib/regex/index.ts index c472f8d5d..a57705526 100644 --- a/backend/src/lib/regex/index.ts +++ b/backend/src/lib/regex/index.ts @@ -10,4 +10,4 @@ export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-] export const LdapUrlRegex = new RE2(/^ldaps?:\/\//); -export const GitHubRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/); +export const BasicRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/); diff --git a/backend/src/server/plugins/secret-scanner-v2.ts b/backend/src/server/plugins/secret-scanner-v2.ts index 466450180..1323fa4ad 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"; @@ -63,4 +65,52 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide return res.send("ok"); } }); + + // 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" }) + }), + headers: z + .object({ + "x-hub-signature": z.string().min(1, { message: "X-Hub-Signature header is required" }) + }) + .passthrough() + }, + config: { + rateLimit: writeLimit + }, + handler: async (req, res) => { + const { dataSourceId } = req.query; + + // Verify signature + const signature = req.headers["x-hub-signature"]; + if (!signature) { + logger.error("Missing X-Hub-Signature header for Bitbucket webhook"); + return res.status(401).send({ message: "Unauthorized: Missing signature" }); + } + + const expectedSignaturePrefix = "sha256="; + if (!signature.startsWith(expectedSignaturePrefix)) { + logger.error({ signature }, "Invalid X-Hub-Signature format for Bitbucket webhook"); + return res.status(401).send({ message: "Unauthorized: Invalid signature format" }); + } + + const receivedSignature = signature.substring(expectedSignaturePrefix.length); + + if (!dataSourceId) return res.status(400).send({ message: "Data Source ID is required" }); + + await server.services.secretScanningV2.bitbucket.handlePushEvent({ + ...(req.body as TBitbucketPushEvent), + dataSourceId, + receivedSignature, + bodyString: JSON.stringify(req.body) + }); + + 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 032dd939e..35ec330e8 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 @@ -118,6 +122,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedFlyioConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options, + ...SanitizedBitbucketConnectionSchema.options, ...SanitizedZabbixConnectionSchema.options ]); @@ -151,6 +156,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ FlyioConnectionListItemSchema, GitLabConnectionListItemSchema, CloudflareConnectionListItemSchema, + BitbucketConnectionListItemSchema, ZabbixConnectionListItemSchema ]); 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..7fe5113e5 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -0,0 +1,88 @@ +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"; + +export const registerBitbucketConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Bitbucket, + server, + 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`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + workspaceSlug: z.string().min(1).max(255) + }), + response: { + 200: z.object({ + repositories: z.object({ slug: z.string(), full_name: z.string(), uuid: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId }, + query: { workspaceSlug } + } = req; + + 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 35958ddf8..56005beac 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"; @@ -64,5 +65,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { getFlyioConnectionListItem(), getGitLabConnectionListItem(), getCloudflareConnectionListItem(), + getBitbucketConnectionListItem(), getZabbixConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -219,6 +225,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.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -256,6 +263,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.APIToken: + case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: @@ -337,6 +345,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, + [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Zabbix]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 342e39d71..cf5ed1a42 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -30,6 +30,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", [AppConnection.Cloudflare]: "Cloudflare", + [AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Zabbix]: "Zabbix" }; @@ -63,5 +64,6 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + 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 }) => { + try { + 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({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } +}; + +export const validateBitbucketConnectionCredentials = async (config: TBitbucketConnectionConfig) => { + await getBitbucketUser(config.credentials); + return config.credentials; +}; + +interface BitbucketWorkspacesResponse { + values: TBitbucketWorkspace[]; + next?: string; +} + +export const listBitbucketWorkspaces = async (appConnection: TBitbucketConnection) => { + const { email, apiToken } = appConnection.credentials; + + const headers = { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + }; + + 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/${encodeURIComponent(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: BitbucketRepositoriesResponse } = await request.get( + 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-schemas.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts new file mode 100644 index 000000000..fab1bf74c --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-schemas.ts @@ -0,0 +1,72 @@ +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") + .max(255) + .describe(AppConnections.CREDENTIALS.BITBUCKET.apiToken), + email: z + .string() + .email() + .trim() + .min(1, "Email required") + .max(255) + .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..f08a8d276 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -0,0 +1,33 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns"; +import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const bitbucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const workspaces = await listBitbucketWorkspaces(appConnection); + return workspaces; + }; + + 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 new file mode 100644 index 000000000..b0694c6e3 --- /dev/null +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -0,0 +1,40 @@ +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 TGetBitbucketRepositoriesDTO = { + connectionId: string; + workspaceSlug: string; +}; + +export type TBitbucketWorkspace = { + slug: string; +}; + +export type TBitbucketRepo = { + uuid: string; + full_name: string; // workspace-slug/repo-slug + slug: 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/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/api-reference/endpoints/app-connections/bitbucket/available.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/available.mdx new file mode 100644 index 000000000..cdaaceff3 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/bitbucket/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/bitbucket/create.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/create.mdx new file mode 100644 index 000000000..70f48aae7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/bitbucket" +--- + + + Check out the configuration docs for [Bitbucket Connections](/integrations/app-connections/bitbucket) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/bitbucket/delete.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/delete.mdx new file mode 100644 index 000000000..a61b6bbb1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/bitbucket/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/bitbucket/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/get-by-id.mdx new file mode 100644 index 000000000..83f7f7521 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/bitbucket/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/bitbucket/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/get-by-name.mdx new file mode 100644 index 000000000..f2aa16f40 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/bitbucket/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/bitbucket/list.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/list.mdx new file mode 100644 index 000000000..4bc4c4714 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/bitbucket" +--- diff --git a/docs/api-reference/endpoints/app-connections/bitbucket/update.mdx b/docs/api-reference/endpoints/app-connections/bitbucket/update.mdx new file mode 100644 index 000000000..289e96681 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/bitbucket/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/bitbucket/{connectionId}" +--- + + + Check out the configuration docs for [Bitbucket Connections](/integrations/app-connections/bitbucket) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/create.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/create.mdx new file mode 100644 index 000000000..eeee8e019 --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-scanning/data-sources/bitbucket" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/delete.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/delete.mdx new file mode 100644 index 000000000..b2bb37d0f --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-id.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-id.mdx new file mode 100644 index 000000000..009d57dcb --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-name.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-name.mdx new file mode 100644 index 000000000..d9b82fb9b --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-scanning/data-sources/bitbucket/data-source-name/{dataSourceName}" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-resources.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-resources.mdx new file mode 100644 index 000000000..e80a71eb6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-resources.mdx @@ -0,0 +1,4 @@ +--- +title: "List Resources" +openapi: "GET /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}/resources" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-scans.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-scans.mdx new file mode 100644 index 000000000..1c1c83abc --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-scans.mdx @@ -0,0 +1,4 @@ +--- +title: "List Scans" +openapi: "GET /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}/scans" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list.mdx new file mode 100644 index 000000000..f75e3c2b7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-scanning/data-sources/bitbucket" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan-resource.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan-resource.mdx new file mode 100644 index 000000000..03d0c79b0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan-resource.mdx @@ -0,0 +1,4 @@ +--- +title: "Scan Resource" +openapi: "POST /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}/resources/{resourceId}/scan" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan.mdx new file mode 100644 index 000000000..f7b7c2dae --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan.mdx @@ -0,0 +1,4 @@ +--- +title: "Scan" +openapi: "POST /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}/scan" +--- diff --git a/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/update.mdx b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/update.mdx new file mode 100644 index 000000000..92a88b6a6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-scanning/data-sources/bitbucket/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-scanning/data-sources/bitbucket/{dataSourceId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 2aa545375..94046ad02 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -210,6 +210,7 @@ "group": "Secret Scanning", "pages": [ "documentation/platform/secret-scanning/overview", + "documentation/platform/secret-scanning/bitbucket", "documentation/platform/secret-scanning/github" ] } @@ -469,6 +470,7 @@ "integrations/app-connections/azure-client-secrets", "integrations/app-connections/azure-devops", "integrations/app-connections/azure-key-vault", + "integrations/app-connections/bitbucket", "integrations/app-connections/camunda", "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", @@ -1135,6 +1137,21 @@ "pages": [ "api-reference/endpoints/secret-scanning/data-sources/list", "api-reference/endpoints/secret-scanning/data-sources/options", + { + "group": "Bitbucket", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-id", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-name", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-resources", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-scans", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/create", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/update", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/delete", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan-resource" + ] + }, { "group": "GitHub", "pages": [ @@ -1284,6 +1301,18 @@ "api-reference/endpoints/app-connections/azure-key-vault/delete" ] }, + { + "group": "Bitbucket", + "pages": [ + "api-reference/endpoints/app-connections/bitbucket/list", + "api-reference/endpoints/app-connections/bitbucket/available", + "api-reference/endpoints/app-connections/bitbucket/get-by-id", + "api-reference/endpoints/app-connections/bitbucket/get-by-name", + "api-reference/endpoints/app-connections/bitbucket/create", + "api-reference/endpoints/app-connections/bitbucket/update", + "api-reference/endpoints/app-connections/bitbucket/delete" + ] + }, { "group": "Camunda", "pages": [ diff --git a/docs/documentation/platform/secret-scanning/bitbucket.mdx b/docs/documentation/platform/secret-scanning/bitbucket.mdx new file mode 100644 index 000000000..c5feb5b06 --- /dev/null +++ b/docs/documentation/platform/secret-scanning/bitbucket.mdx @@ -0,0 +1,100 @@ +--- +title: "Bitbucket Secret Scanning" +sidebarTitle: "Bitbucket" +description: "Learn how to configure secret scanning for Bitbucket." +--- + +## Prerequisites + +- Create a [Bitbucket Connection](/integrations/app-connections/bitbucket) with Secret Scanning permissions + +## Create a Bitbucket Data Source in Infisical + + + + 1. Navigate to your Secret Scanning Project's Dashboard and click the **Add Data Source** button. + ![Secret Scanning Dashboard](/images/platform/secret-scanning/github/github-data-source-step-1.png) + + 2. Select the **Bitbucket** option. + + ![Select Bitbucket](/images/platform/secret-scanning/bitbucket/step-2.png) + + 3. Configure which workspace and repositories you would like to scan. Then click **Next**. + ![Data Source Configuration](/images/platform/secret-scanning/bitbucket/step-3.png) + + - **Bitbucket Connection** - the connection that has access to the repositories you want to scan. + - **Workspace** - the Bitbucket workspace to scan secrets in. + - **Scan Repositories** - select which repositories you would like to scan. + - **All Repositories** - Infisical will scan all repositories associated with your connection. + - **Select Repositories** - Infisical will scan the selected repositories. + - **Auto-Scan Enabled** - whether Infisical should automatically perform a scan when a push is made to configured repositories. + + 4. Give your data source a name and description (optional). Then click **Next**. + ![Data Source Details](/images/platform/secret-scanning/bitbucket/step-4.png) + + - **Name** - the name of the data source. Must be slug-friendly. + - **Description** (optional) - a description of this data source. + + 5. Review your data source, then click **Create Data Source**. + ![Data Source Review](/images/platform/secret-scanning/bitbucket/step-5.png) + + 6. Your **Bitbucket Data Source** is now available and will begin a full scan if **Auto-Scan** is enabled. + ![Data Source Created](/images/platform/secret-scanning/bitbucket/step-6.png) + + 7. You can view repositories and scan results by clicking on your data source. + ![Data Source Page](/images/platform/secret-scanning/bitbucket/step-7.png) + + 8. In addition, you can review any findings from the **Findings Page**. + ![Findings Page](/images/platform/secret-scanning/bitbucket/step-8.png) + + + To create a Bitbucket Data Source, make an API request to the [Create Bitbucket Data Source](/api-reference/endpoints/secret-scanning/data-sources/bitbucket/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-scanning/data-sources/bitbucket \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-bitbucket-source", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my bitbucket data source", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "isAutoScanEnabled": true, + "config": { + "workspaceSlug": "my-workspace", + "includeRepos": ["*"] + } + }' + ``` + + ### Sample response + + ```bash Response + { + "dataSource": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "externalId": "1234567890", + "name": "my-bitbucket-source", + "description": "my bitbucket data source", + "isAutoScanEnabled": true, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "type": "bitbucket", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "bitbucket", + "name": "my-bitbucket-app", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "config": { + "workspaceSlug": "my-workspace", + "includeRepos": ["*"] + } + } + } + ``` + + diff --git a/docs/images/app-connections/bitbucket/step-1.png b/docs/images/app-connections/bitbucket/step-1.png new file mode 100644 index 000000000..3b5941e82 Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-1.png differ diff --git a/docs/images/app-connections/bitbucket/step-2.png b/docs/images/app-connections/bitbucket/step-2.png new file mode 100644 index 000000000..91d06eb34 Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-2.png differ diff --git a/docs/images/app-connections/bitbucket/step-3.png b/docs/images/app-connections/bitbucket/step-3.png new file mode 100644 index 000000000..840d86e1b Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-3.png differ diff --git a/docs/images/app-connections/bitbucket/step-4.png b/docs/images/app-connections/bitbucket/step-4.png new file mode 100644 index 000000000..98949dd3c Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-4.png differ diff --git a/docs/images/app-connections/bitbucket/step-5.png b/docs/images/app-connections/bitbucket/step-5.png new file mode 100644 index 000000000..278446897 Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-5.png differ diff --git a/docs/images/app-connections/bitbucket/step-6.png b/docs/images/app-connections/bitbucket/step-6.png new file mode 100644 index 000000000..cf45c347b Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-6.png differ diff --git a/docs/images/app-connections/bitbucket/step-7.png b/docs/images/app-connections/bitbucket/step-7.png new file mode 100644 index 000000000..4c13a1f61 Binary files /dev/null and b/docs/images/app-connections/bitbucket/step-7.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-2.png b/docs/images/platform/secret-scanning/bitbucket/step-2.png new file mode 100644 index 000000000..0d10951a4 Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-2.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-3.png b/docs/images/platform/secret-scanning/bitbucket/step-3.png new file mode 100644 index 000000000..97c373711 Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-3.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-4.png b/docs/images/platform/secret-scanning/bitbucket/step-4.png new file mode 100644 index 000000000..2a9fb1cb1 Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-4.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-5.png b/docs/images/platform/secret-scanning/bitbucket/step-5.png new file mode 100644 index 000000000..365323c6d Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-5.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-6.png b/docs/images/platform/secret-scanning/bitbucket/step-6.png new file mode 100644 index 000000000..adac9bb40 Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-6.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-7.png b/docs/images/platform/secret-scanning/bitbucket/step-7.png new file mode 100644 index 000000000..2ecbdda1a Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-7.png differ diff --git a/docs/images/platform/secret-scanning/bitbucket/step-8.png b/docs/images/platform/secret-scanning/bitbucket/step-8.png new file mode 100644 index 000000000..3948374f6 Binary files /dev/null and b/docs/images/platform/secret-scanning/bitbucket/step-8.png differ diff --git a/docs/integrations/app-connections/1password.mdx b/docs/integrations/app-connections/1password.mdx index 0c3926a1b..394d8bc23 100644 --- a/docs/integrations/app-connections/1password.mdx +++ b/docs/integrations/app-connections/1password.mdx @@ -92,7 +92,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com "method": "api-token", "credentials": { "instanceUrl": "https://1pass.example.com", - "apiToken": "[PRIVATE TOKEN]" + "apiToken": "" } }' ``` diff --git a/docs/integrations/app-connections/bitbucket.mdx b/docs/integrations/app-connections/bitbucket.mdx new file mode 100644 index 000000000..e4f9ae29f --- /dev/null +++ b/docs/integrations/app-connections/bitbucket.mdx @@ -0,0 +1,133 @@ +--- +title: "Bitbucket Connection" +description: "Learn how to configure a Bitbucket Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/) to connect with Bitbucket. + + + Infisical recommends creating a dedicated Bitbucket account with access restricted to only the resources your use case requires. + + +## Create Bitbucket Access Token + + + + Go to [Account API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens) and click **Create API token with scopes**. + + ![Create API Token](/images/app-connections/bitbucket/step-1.png) + + + Set the name and expiration date of the token, then click **Next**. + + ![Set Name and Expiry](/images/app-connections/bitbucket/step-2.png) + + + Keep in mind that you'll need to manually replace the token after it expires. + + + + Select **Bitbucket** and then click **Next**. + + ![Select Bitbucket](/images/app-connections/bitbucket/step-3.png) + + + Configure permissions according to your app's use case: + + + + ``` + read:workspace:bitbucket + read:user:bitbucket + read:webhook:bitbucket + write:webhook:bitbucket + delete:webhook:bitbucket + read:repository:bitbucket + ``` + + ![Configure Permissions](/images/app-connections/bitbucket/step-4.png) + + + + Click **Next**. + + + + Save the API Token for later steps. + + ![Save Token](/images/app-connections/bitbucket/step-5.png) + + + +## Create Bitbucket Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **Add new connection** button and select **Bitbucket** from the list of available connections. + + + Complete the Bitbucket Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - Your Bitbucket email + - The API Token from earlier steps + + ![Bitbucket Connection Modal](/images/app-connections/bitbucket/step-6.png) + + + After clicking Create, your **Bitbucket Connection** is established and ready to use with your Infisical projects. + + ![Bitbucket Connection Created](/images/app-connections/bitbucket/step-7.png) + + + + + To create a Bitbucket Connection, make an API request to the [Create Bitbucket Connection](/api-reference/endpoints/app-connections/bitbucket/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/bitbucket \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-bitbucket-connection", + "method": "api-token", + "credentials": { + "email": "user@example.com", + "apiToken": "" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-bitbucket-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "bitbucket", + "method": "api-token", + "credentials": { + "email": "user@example.com" + } + } + } + ``` + + 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 5e9121e12..42d289804 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/public/images/integrations/BitBucket.png b/frontend/public/images/integrations/Bitbucket.png similarity index 100% rename from frontend/public/images/integrations/BitBucket.png rename to frontend/public/images/integrations/Bitbucket.png 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..092e7d623 --- /dev/null +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConfigFields/BitbucketDataSourceConfigFields.tsx @@ -0,0 +1,172 @@ +import { useEffect } from "react"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; +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, + TBitbucketWorkspace, + useBitbucketConnectionListRepositories, + useBitbucketConnectionListWorkspaces +} 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 selectedWorkspaceSlug = useWatch({ control, name: "config.workspaceSlug" }); + + const { data: workspaces, isPending: areWorkspacesLoading } = + useBitbucketConnectionListWorkspaces(connectionId, { enabled: Boolean(connectionId) }); + + const { data: repositories, isPending: areRepositoriesLoading } = + useBitbucketConnectionListRepositories(connectionId, selectedWorkspaceSlug, { + enabled: Boolean(connectionId) && Boolean(selectedWorkspaceSlug) + }); + + 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.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} + /> + + )} + /> + + + + {scanMethod === ScanMethod.SelectRepositories && ( + ( + Ensure that your connection has the correct permissions.} + > +
+ Don't see the repository you're looking for?{" "} + +
+ + } + > + + (value as string[]).includes(repository.full_name) + )} + onChange={(newValue) => { + onChange( + newValue ? (newValue as MultiValue).map((p) => p.full_name) : [] + ); + }} + options={repositories} + placeholder="Select repositories..." + getOptionLabel={(option) => option.full_name} + getOptionValue={(option) => option.full_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..cbdc0f0b8 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..1523cb0ad --- /dev/null +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceReviewFields/BitbucketDataSourceReviewFields.tsx @@ -0,0 +1,28 @@ +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, workspaceSlug }, connection] = watch(["config", "connection"]); + const shouldScanAll = includeRepos[0] === "*"; + + return ( + + {connection && {connection.name}} + {workspaceSlug} + + {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..e29dc9ebd 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..38345ca07 --- /dev/null +++ b/frontend/src/components/secret-scanning/forms/schemas/bitbucket-data-source-schema.ts @@ -0,0 +1,19 @@ +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({ + workspaceSlug: z.string().min(1, "Workspace Required").max(128), + 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..23e6d86e4 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/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx index dd54dbf63..15169c73a 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx @@ -60,7 +60,7 @@ export const OnePassSyncFields = () => { menuPlacement="top" isLoading={isVaultsLoading && Boolean(connectionId)} isDisabled={!connectionId} - value={vaults?.find((v) => v.id === value) ?? null} + value={vaults?.find((v) => v.id === value) || null} onChange={(option) => onChange((option as SingleValue)?.id ?? null)} options={vaults} placeholder="Select a vault..." diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx index 26fa601f6..12092e301 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx @@ -120,7 +120,7 @@ export const OCIVaultSyncFields = () => { menuPlacement="top" isLoading={isVaultsLoading && Boolean(connectionId)} isDisabled={!connectionId || !selectedCompartment} - value={vaults?.find((v) => v.id === value) ?? null} + value={vaults?.find((v) => v.id === value) || null} onChange={(option) => { onChange((option as SingleValue<{ id: string }>)?.id ?? null); setValue("destinationConfig.keyOcid", ""); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 321555f45..fa6f11bbb 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -40,6 +40,7 @@ import { WindmillConnectionMethod, ZabbixConnectionMethod } 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"; @@ -90,6 +91,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, + [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" } }; @@ -122,6 +124,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.ApiToken: + case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/helpers/secretScanningV2.ts b/frontend/src/helpers/secretScanningV2.ts index 877a57468..8021142b5 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/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..bfae0534f --- /dev/null +++ b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx @@ -0,0 +1,70 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { + TBitbucketConnectionListRepositoriesResponse, + TBitbucketConnectionListWorkspacesResponse, + TBitbucketRepo, + TBitbucketWorkspace +} from "./types"; + +const bitbucketConnectionKeys = { + all: [...appConnectionKeys.all, "bitbucket"] as const, + listRepos: (connectionId: string, workspaceSlug: string) => + [...bitbucketConnectionKeys.all, "repos", connectionId, workspaceSlug] as const, + listWorkspaces: (connectionId: string) => + [...bitbucketConnectionKeys.all, "workspaces", connectionId] as const +}; + +export const useBitbucketConnectionListWorkspaces = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TBitbucketWorkspace[], + unknown, + TBitbucketWorkspace[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: bitbucketConnectionKeys.listWorkspaces(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/bitbucket/${connectionId}/workspaces` + ); + + return data.workspaces; + }, + ...options + }); +}; + +export const useBitbucketConnectionListRepositories = ( + connectionId: string, + workspaceSlug: string, + options?: Omit< + UseQueryOptions< + TBitbucketRepo[], + unknown, + TBitbucketRepo[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: bitbucketConnectionKeys.listRepos(connectionId, workspaceSlug), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/bitbucket/${connectionId}/repositories?workspaceSlug=${encodeURIComponent(workspaceSlug)}` + ); + + return data.repositories; + }, + ...options + }); +}; 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..e7e653a93 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/bitbucket/types.ts @@ -0,0 +1,17 @@ +export type TBitbucketWorkspace = { + slug: string; +}; + +export type TBitbucketRepo = { + uuid: string; + slug: string; + full_name: string; // workspace-slug/repo-slug +}; + +export type TBitbucketConnectionListWorkspacesResponse = { + workspaces: TBitbucketWorkspace[]; +}; + +export type TBitbucketConnectionListRepositoriesResponse = { + repositories: TBitbucketRepo[]; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 38de5b1b3..31443c67d 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -28,5 +28,6 @@ export enum AppConnection { Flyio = "flyio", Gitlab = "gitlab", Cloudflare = "cloudflare", + Bitbucket = "bitbucket", Zabbix = "zabbix" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 049f75471..e7d672a34 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 TZabbixConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Zabbix; }; @@ -164,6 +168,7 @@ export type TAppConnectionOption = | TFlyioConnectionOption | TGitlabConnectionOption | TCloudflareConnectionOption + | TBitbucketConnectionOption | TZabbixConnectionOption; export type TAppConnectionOptionMap = { @@ -196,5 +201,6 @@ export type TAppConnectionOptionMap = { [AppConnection.Flyio]: TFlyioConnectionOption; [AppConnection.Gitlab]: TGitlabConnectionOption; [AppConnection.Cloudflare]: TCloudflareConnectionOption; + [AppConnection.Bitbucket]: TBitbucketConnectionOption; [AppConnection.Zabbix]: TZabbixConnectionOption; }; 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..42b4129e0 --- /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 3bcf4fe5c..4aabde9ad 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"; @@ -38,6 +39,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"; @@ -92,6 +94,7 @@ export type TAppConnection = | TFlyioConnection | TGitLabConnection | TCloudflareConnection + | TBitbucketConnection | TZabbixConnection; export type TAvailableAppConnection = Pick; @@ -149,5 +152,6 @@ export type TAppConnectionMap = { [AppConnection.Flyio]: TFlyioConnection; [AppConnection.Gitlab]: TGitLabConnection; [AppConnection.Cloudflare]: TCloudflareConnection; + [AppConnection.Bitbucket]: TBitbucketConnection; [AppConnection.Zabbix]: TZabbixConnection; }; diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index eab946ba6..b70ee2ab9 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -4,7 +4,7 @@ export { useDeleteIntegrationAuth, useDeleteIntegrationAuths, useGetIntegrationAuthApps, - useGetIntegrationAuthBitBucketWorkspaces, + useGetIntegrationAuthBitbucketWorkspaces, useGetIntegrationAuthById, useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthCircleCIOrganizations, diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index f62887043..044ec8d2a 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -6,8 +6,8 @@ import { TReactQueryOptions } from "@app/types/reactQuery"; import { workspaceKeys } from "../workspace"; import { App, - BitBucketEnvironment, - BitBucketWorkspace, + BitbucketEnvironment, + BitbucketWorkspace, ChecklyGroup, CircleCIOrganization, Environment, @@ -98,9 +98,9 @@ const integrationAuthKeys = { integrationAuthId: string; appId: string; }) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const, - getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) => + getIntegrationAuthBitbucketWorkspaces: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const, - getIntegrationAuthBitBucketEnvironments: ( + getIntegrationAuthBitbucketEnvironments: ( integrationAuthId: string, workspaceSlug: string, repoSlug: string @@ -446,23 +446,23 @@ const fetchIntegrationAuthRailwayServices = async ({ return services; }; -const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => { +const fetchIntegrationAuthBitbucketWorkspaces = async (integrationAuthId: string) => { const { data: { workspaces } - } = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>( + } = await apiRequest.get<{ workspaces: BitbucketWorkspace[] }>( `/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces` ); return workspaces; }; -const fetchIntegrationAuthBitBucketEnvironments = async ( +const fetchIntegrationAuthBitbucketEnvironments = async ( integrationAuthId: string, workspaceSlug: string, repoSlug: string ) => { const { data: { environments } - } = await apiRequest.get<{ environments: BitBucketEnvironment[] }>( + } = await apiRequest.get<{ environments: BitbucketEnvironment[] }>( `/api/v1/integration-auth/${integrationAuthId}/bitbucket/environments`, { params: { @@ -833,10 +833,10 @@ export const useGetIntegrationAuthRailwayServices = ({ }); }; -export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => { +export const useGetIntegrationAuthBitbucketWorkspaces = (integrationAuthId: string) => { return useQuery({ - queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId), - queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId), + queryKey: integrationAuthKeys.getIntegrationAuthBitbucketWorkspaces(integrationAuthId), + queryFn: () => fetchIntegrationAuthBitbucketWorkspaces(integrationAuthId), enabled: true }); }; @@ -858,7 +858,7 @@ export const useGetIntegrationAuthOctopusDeployScopeValues = ( ...options }); -export const useGetIntegrationAuthBitBucketEnvironments = ( +export const useGetIntegrationAuthBitbucketEnvironments = ( { integrationAuthId, workspaceSlug, @@ -871,13 +871,13 @@ export const useGetIntegrationAuthBitBucketEnvironments = ( options?: TReactQueryOptions["options"] ) => { return useQuery({ - queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments( + queryKey: integrationAuthKeys.getIntegrationAuthBitbucketEnvironments( integrationAuthId, workspaceSlug, repoSlug ), queryFn: () => - fetchIntegrationAuthBitBucketEnvironments(integrationAuthId, workspaceSlug, repoSlug), + fetchIntegrationAuthBitbucketEnvironments(integrationAuthId, workspaceSlug, repoSlug), ...options }); }; diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index b57e5aeca..a808e9e18 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -78,13 +78,13 @@ export type Service = { serviceId: string; }; -export type BitBucketWorkspace = { +export type BitbucketWorkspace = { uuid: string; name: string; slug: string; }; -export type BitBucketEnvironment = { +export type BitbucketEnvironment = { uuid: string; name: string; slug: string; diff --git a/frontend/src/hooks/api/secretScanningV2/enums.ts b/frontend/src/hooks/api/secretScanningV2/enums.ts index 082f3d760..40e5ea7dd 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..d9e7eb929 --- /dev/null +++ b/frontend/src/hooks/api/secretScanningV2/types/bitbucket-data-source.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { SecretScanningDataSource } from "../enums"; +import { TSecretScanningDataSourceBase } from "./shared"; + +export type TBitbucketDataSource = TSecretScanningDataSourceBase & { + type: SecretScanningDataSource.Bitbucket; + config: { + workspaceSlug: string; + 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..078a7051d 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/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 140857110..0e080d6dd 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"; @@ -135,6 +136,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.Bitbucket: + return ; case AppConnection.Zabbix: return ; default: @@ -231,6 +234,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.Bitbucket: + return ; case AppConnection.Zabbix: return ; default: 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..7f283d3be --- /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)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx index ba4d9babe..291dec6ca 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx @@ -20,9 +20,9 @@ import { useWorkspace } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthApps, - useGetIntegrationAuthBitBucketWorkspaces + useGetIntegrationAuthBitbucketWorkspaces } from "@app/hooks/api"; -import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries"; +import { useGetIntegrationAuthBitbucketEnvironments } from "@app/hooks/api/integrationAuth/queries"; import { IntegrationsListPageTabs } from "@app/types/integrations"; enum BitbucketScope { @@ -89,8 +89,8 @@ export const BitbucketConfigurePage = () => { } }); - const bitBucketWorkspace = watch("targetWorkspace"); - const bitBucketRepo = watch("targetRepo"); + const bitbucketWorkspace = watch("targetWorkspace"); + const bitbucketRepo = watch("targetRepo"); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.BitbucketConfigurePage.id, @@ -99,21 +99,21 @@ export const BitbucketConfigurePage = () => { const { currentWorkspace } = useWorkspace(); const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } = - useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? ""); + useGetIntegrationAuthBitbucketWorkspaces((integrationAuthId as string) ?? ""); const { data: bitbucketRepos, isPending: isBitbucketReposLoading } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "", - workspaceSlug: bitBucketWorkspace?.slug + workspaceSlug: bitbucketWorkspace?.slug }); const { data: bitbucketEnvironments, isPending: isBitbucketEnvironmentsLoading } = - useGetIntegrationAuthBitBucketEnvironments( + useGetIntegrationAuthBitbucketEnvironments( { integrationAuthId: (integrationAuthId as string) ?? "", - workspaceSlug: bitBucketWorkspace?.slug ?? "", - repoSlug: bitBucketRepo?.appId ?? "" + workspaceSlug: bitbucketWorkspace?.slug ?? "", + repoSlug: bitbucketRepo?.appId ?? "" }, - { enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) } + { enabled: Boolean(bitbucketWorkspace?.slug && bitbucketRepo?.appId) } ); const onSubmit = async ({ @@ -167,8 +167,8 @@ export const BitbucketConfigurePage = () => { useEffect(() => { if ( - bitBucketWorkspace || - bitBucketRepo || + bitbucketWorkspace || + bitbucketRepo || !bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace @@ -325,7 +325,7 @@ export const BitbucketConfigurePage = () => { ? "Select an environment..." : "No environments found..." } - isLoading={isBitbucketEnvironmentsLoading && Boolean(bitBucketRepo)} + isLoading={isBitbucketEnvironmentsLoading && Boolean(bitbucketRepo)} isDisabled={!bitbucketEnvironments?.length || isBitbucketEnvironmentsLoading} /> 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..7115b16a6 --- /dev/null +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/BitbucketDataSourceConfigDisplay.tsx @@ -0,0 +1,21 @@ +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, workspaceSlug } + } = dataSource; + + return ( + <> + {workspaceSlug} + + {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..874dc034f 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay.tsx @@ -3,6 +3,7 @@ import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; +import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay"; import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay"; type Props = { @@ -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}` diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceTable.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceTable.tsx index 8a25c4a88..ae671c49a 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceTable.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceTable.tsx @@ -112,10 +112,12 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => { const isActive = includeRepos.includes("*") || includeRepos.includes(name); if (filters.status.length === 1) { - if (filters.status.includes(ResourceStatus.Active)) { - return isActive; + if (filters.status.includes(ResourceStatus.Active) && !isActive) { + return false; + } + if (filters.status.includes(ResourceStatus.Inactive) && isActive) { + return false; } - return !isActive; } const searchValue = search.trim().toLowerCase(); diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx index f07e229a9..2e6698a48 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx @@ -294,7 +294,7 @@ export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => { - +
PlatformPlatform
Name