feat(secret-scanning): BitBucket data source

This commit is contained in:
x032205
2025-07-03 00:01:37 -04:00
parent ae35a863bc
commit edecfb1f62
30 changed files with 796 additions and 54 deletions

View File

@@ -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
});

View File

@@ -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<void>
> = {
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
[SecretScanningDataSource.BitBucket]: registerBitBucketSecretScanningRouter
};

View File

@@ -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({

View File

@@ -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
};

View File

@@ -0,0 +1,252 @@
import { join } from "path";
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import {
SecretScanningDataSource,
SecretScanningFindingSeverity,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
cloneRepository,
convertPatchLineToFileLineNumber,
replaceNonChangesWithNewlines
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
import {
TSecretScanningFactoryGetDiffScanFindingsPayload,
TSecretScanningFactoryGetDiffScanResourcePayload,
TSecretScanningFactoryGetFullScanPath,
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { titleCaseToCamelCase } from "@app/lib/fn";
import { GitHubRepositoryRegex } from "@app/lib/regex";
import {
getBitBucketUser,
listBitBucketRepositories,
TBitBucketConnection
} from "@app/services/app-connection/bitbucket";
import { TBitBucketDataSourceWithConnection, TQueueBitBucketResourceDiffScan } from "./bitbucket-secret-scanning-types";
export const BitBucketSecretScanningFactory = () => {
const initialize: TSecretScanningFactoryInitialize<TBitBucketConnection> = async (
{ connection, secretScanningV2DAL },
callback
) => {
// TODO(andrey): Swap for something proper
const externalId = connection.credentials.email;
const existingDataSource = await secretScanningV2DAL.dataSources.findOne({
externalId,
type: SecretScanningDataSource.BitBucket
});
if (existingDataSource)
throw new BadRequestError({
message: `A Data Source already exists for this BitBucket Radar Connection in the Project with ID "${existingDataSource.projectId}"`
});
return callback({
externalId
});
};
const postInitialization: TSecretScanningFactoryPostInitialization<TBitBucketConnection> = async () => {
// no post-initialization required
};
const listRawResources: TSecretScanningFactoryListRawResources<TBitBucketDataSourceWithConnection> = async (
dataSource
) => {
const {
connection,
config: { includeRepos }
} = dataSource;
const repos = await listBitBucketRepositories(connection);
const filteredRepos: typeof repos = [];
if (includeRepos.includes("*")) {
filteredRepos.push(...repos);
} else {
filteredRepos.push(...repos.filter((repo) => includeRepos.includes(repo.full_name)));
}
return filteredRepos.map(({ slug, full_name }) => ({
name: full_name,
externalId: slug.toString(),
type: SecretScanningResource.Repository
}));
};
// TODO(andrey): Finish
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TBitBucketDataSourceWithConnection> = async ({
dataSource,
resourceName,
tempFolder
}) => {
const {
connection: {
credentials: { apiToken, email }
}
} = dataSource;
const repoPath = join(tempFolder, "repo.git");
if (!GitHubRepositoryRegex.test(resourceName)) {
throw new Error("Invalid BitBucket repository name");
}
const { username } = await getBitBucketUser({ email, apiToken });
await cloneRepository({
cloneUrl: `https://${encodeURIComponent(username)}:${apiToken}@bitbucket.org/${resourceName}.git`,
repoPath
});
return repoPath;
};
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
TQueueBitBucketResourceDiffScan["payload"]
> = ({ repository }) => {
return {
name: repository.full_name,
externalId: repository.id.toString(),
type: SecretScanningResource.Repository
};
};
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
TBitBucketDataSourceWithConnection,
TQueueBitBucketResourceDiffScan["payload"]
> = async ({ dataSource, payload, resourceName, configPath }) => {
const {
connection: {
credentials: { apiToken, email }
}
} = dataSource;
console.log("getDiffScanFindingsPayload");
const { commits, repository } = payload;
const allFindings: SecretMatch[] = [];
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
for (const commit of commits) {
// eslint-disable-next-line no-await-in-loop
const { data: diffstat } = await request.get<{
values: {
status: "added" | "modified" | "removed" | "renamed";
new?: { path: string };
old?: { path: string };
}[];
}>(`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diffstat/${commit.id}`, {
headers: {
Authorization: authHeader,
Accept: "application/json"
}
});
// eslint-disable-next-line no-continue
if (!diffstat.values) continue;
for (const file of diffstat.values) {
if ((file.status === "added" || file.status === "modified") && file.new?.path) {
const filePath = file.new.path;
// eslint-disable-next-line no-await-in-loop
const { data: patch } = await request.get<string>(
`https://api.bitbucket.org/2.0/repositories/${repository.full_name}/diff/${commit.id}`,
{
params: {
path: filePath
},
headers: {
Authorization: authHeader
},
responseType: "text"
}
);
console.log(1);
// eslint-disable-next-line no-continue
if (!patch) continue;
console.log(2);
// eslint-disable-next-line
const findings = await scanContentAndGetFindings(replaceNonChangesWithNewlines(`\n${patch}`), configPath);
console.log(3);
console.log(findings);
const adjustedFindings = findings.map((finding) => {
const startLine = convertPatchLineToFileLineNumber(patch, finding.StartLine);
const endLine =
finding.StartLine === finding.EndLine
? startLine
: convertPatchLineToFileLineNumber(patch, finding.EndLine);
const startColumn = finding.StartColumn - 1; // subtract 1 for +
const endColumn = finding.EndColumn - 1; // subtract 1 for +
console.log("finding");
console.log(finding.Link);
return {
...finding,
StartLine: startLine,
EndLine: endLine,
StartColumn: startColumn,
EndColumn: endColumn,
File: filePath,
Commit: commit.id,
Author: commit.author.name,
Email: commit.author.email ?? "",
Message: commit.message,
Fingerprint: `${commit.id}:${filePath}:${finding.RuleID}:${startLine}:${startColumn}`,
Date: commit.timestamp,
Link: `https://bitbucket.org/${resourceName}/src/${commit.id}/${filePath}#lines-${startLine}`
};
});
console.log("adjusted");
console.log(adjustedFindings);
allFindings.push(...adjustedFindings);
}
}
}
console.log("HEREEE");
console.log(allFindings);
return allFindings.map(
({
// discard match and secret as we don't want to store
Match,
Secret,
...finding
}) => ({
details: titleCaseToCamelCase(finding),
fingerprint: finding.Fingerprint,
severity: SecretScanningFindingSeverity.High,
rule: finding.RuleID
})
);
};
return {
initialize,
postInitialization,
listRawResources,
getFullScanPath,
getDiffScanResourcePayload,
getDiffScanFindingsPayload
};
};

View File

@@ -0,0 +1,87 @@
import { z } from "zod";
import {
SecretScanningDataSource,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
BaseCreateSecretScanningDataSourceSchema,
BaseSecretScanningDataSourceSchema,
BaseSecretScanningFindingSchema,
BaseUpdateSecretScanningDataSourceSchema,
GitRepositoryScanFindingDetailsSchema
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas";
import { SecretScanningDataSources } from "@app/lib/api-docs";
import { GitHubRepositoryRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const BitBucketDataSourceConfigSchema = z.object({
includeRepos: z
.array(
z
.string()
.min(1)
.max(256)
.refine((value) => value === "*" || GitHubRepositoryRegex.test(value), "Invalid repository name format")
)
.nonempty("One or more repositories required")
.max(100, "Cannot configure more than 100 repositories")
.default(["*"])
.describe(SecretScanningDataSources.CONFIG.BITBUCKET.includeRepos)
});
export const BitBucketDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.BitBucket,
isConnectionRequired: true
})
.extend({
config: BitBucketDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "BitBucket"
})
);
export const CreateBitBucketDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.BitBucket,
isConnectionRequired: true
})
.extend({
config: BitBucketDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "BitBucket"
})
);
export const UpdateBitBucketDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(
SecretScanningDataSource.BitBucket
)
.extend({
config: BitBucketDataSourceConfigSchema.optional()
})
.describe(
JSON.stringify({
title: "BitBucket"
})
);
export const BitBucketDataSourceListItemSchema = z
.object({
name: z.literal("BitBucket"),
connection: z.literal(AppConnection.BitBucket),
type: z.literal(SecretScanningDataSource.BitBucket)
})
.describe(
JSON.stringify({
title: "BitBucket"
})
);
export const BitBucketFindingSchema = BaseSecretScanningFindingSchema.extend({
resourceType: z.literal(SecretScanningResource.Repository),
dataSourceType: z.literal(SecretScanningDataSource.BitBucket),
details: GitRepositoryScanFindingDetailsSchema
});

View File

@@ -0,0 +1,87 @@
import { PushEvent } from "@octokit/webhooks-types";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
import { logger } from "@app/lib/logger";
import { TBitBucketDataSource } from "./bitbucket-secret-scanning-types";
export const bitBucketSecretScanningService = (
secretScanningV2DAL: TSecretScanningV2DALFactory,
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">
) => {
const handleInstallationDeletedEvent = async (installationId: number) => {
const dataSource = await secretScanningV2DAL.dataSources.findOne({
externalId: String(installationId),
type: SecretScanningDataSource.BitBucket
});
if (!dataSource) {
logger.error(
`secretScanningV2RemoveEvent: BitBucket - Could not find data source [installationId=${installationId}]`
);
return;
}
logger.info(
`secretScanningV2RemoveEvent: BitBucket - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]`
);
await secretScanningV2DAL.dataSources.updateById(dataSource.id, {
isDisconnected: true
});
};
const handlePushEvent = async (payload: PushEvent) => {
const { commits, repository, installation } = payload;
if (!commits || !repository || !installation) {
logger.warn(
`secretScanningV2PushEvent: BitBucket - Insufficient data [commits=${commits?.length ?? 0}] [repository=${repository.name}] [installationId=${installation?.id}]`
);
return;
}
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
externalId: String(installation.id),
type: SecretScanningDataSource.BitBucket
})) as TBitBucketDataSource | undefined;
if (!dataSource) {
logger.error(
`secretScanningV2PushEvent: BitBucket - Could not find data source [installationId=${installation.id}]`
);
return;
}
const {
isAutoScanEnabled,
config: { includeRepos }
} = dataSource;
if (!isAutoScanEnabled) {
logger.info(
`secretScanningV2PushEvent: BitBucket - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [installationId=${installation.id}]`
);
return;
}
if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) {
await secretScanningV2Queue.queueResourceDiffScan({
dataSourceType: SecretScanningDataSource.BitBucket,
payload,
dataSourceId: dataSource.id
});
} else {
logger.info(
`secretScanningV2PushEvent: BitBucket - ignoring due to repository not being present in config [installationId=${installation.id}] [dataSourceId=${dataSource.id}]`
);
}
};
return {
handlePushEvent,
handleInstallationDeletedEvent
};
};

View File

@@ -0,0 +1,32 @@
import { PushEvent } from "@octokit/webhooks-types";
import { z } from "zod";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TBitBucketConnection } from "@app/services/app-connection/bitbucket";
import {
BitBucketDataSourceListItemSchema,
BitBucketDataSourceSchema,
BitBucketFindingSchema,
CreateBitBucketDataSourceSchema
} from "./bitbucket-secret-scanning-schemas";
export type TBitBucketDataSource = z.infer<typeof BitBucketDataSourceSchema>;
export type TBitBucketDataSourceInput = z.infer<typeof CreateBitBucketDataSourceSchema>;
export type TBitBucketDataSourceListItem = z.infer<typeof BitBucketDataSourceListItemSchema>;
export type TBitBucketFinding = z.infer<typeof BitBucketFindingSchema>;
export type TBitBucketDataSourceWithConnection = TBitBucketDataSource & {
connection: TBitBucketConnection;
};
export type TQueueBitBucketResourceDiffScan = {
dataSourceType: SecretScanningDataSource.BitBucket;
payload: PushEvent;
dataSourceId: string;
resourceId: string;
scanId: string;
};

View File

@@ -0,0 +1,3 @@
export * from "./bitbucket-secret-scanning-constants";
export * from "./bitbucket-secret-scanning-schemas";
export * from "./bitbucket-secret-scanning-types";

View File

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

View File

@@ -1,3 +1,4 @@
import { BitBucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
import { SecretScanningDataSource } from "./secret-scanning-v2-enums";
@@ -15,5 +16,6 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory<
>;
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
[SecretScanningDataSource.BitBucket]: BitBucketSecretScanningFactory as TSecretScanningFactoryImplementation
};

View File

@@ -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, TSecretScanningDataSourceListItem> = {
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
[SecretScanningDataSource.BitBucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
};
export const listSecretScanningDataSourceOptions = () => {

View File

@@ -2,13 +2,17 @@ import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/se
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = {
[SecretScanningDataSource.GitHub]: "GitHub"
[SecretScanningDataSource.GitHub]: "GitHub",
[SecretScanningDataSource.BitBucket]: "BitBucket"
};
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 AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
// TODO(andrey): May need change
[SecretScanningDataSource.BitBucket]: { verb: "push", noun: "repositories" }
};

View File

@@ -49,6 +49,7 @@ import { TAppConnection } from "@app/services/app-connection/app-connection-type
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { bitBucketSecretScanningService } from "./bitbucket/bitbucket-secret-scanning-service";
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
@@ -869,6 +870,7 @@ export const secretScanningV2ServiceFactory = ({
updateSecretScanningFindingById,
findSecretScanningConfigByProjectId,
upsertSecretScanningConfig,
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue)
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
bitbucket: bitBucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue)
};
};

View File

@@ -4,6 +4,14 @@ import {
TSecretScanningResources,
TSecretScanningScans
} from "@app/db/schemas";
import {
TBitBucketDataSource,
TBitBucketDataSourceInput,
TBitBucketDataSourceListItem,
TBitBucketDataSourceWithConnection,
TBitBucketFinding,
TQueueBitBucketResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/bitbucket";
import {
TGitHubDataSource,
TGitHubDataSourceInput,
@@ -19,7 +27,7 @@ import {
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export type TSecretScanningDataSource = TGitHubDataSource;
export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt?: Date | null;
@@ -41,13 +49,15 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
resourceName: string;
};
export type TSecretScanningDataSourceWithConnection = TGitHubDataSourceWithConnection;
export type TSecretScanningDataSourceWithConnection =
| TGitHubDataSourceWithConnection
| TBitBucketDataSourceWithConnection;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitBucketDataSourceInput;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitBucketDataSourceListItem;
export type TSecretScanningFinding = TGitHubFinding;
export type TSecretScanningFinding = TGitHubFinding | TBitBucketFinding;
export type TListSecretScanningDataSourcesByProjectId = {
projectId: string;
@@ -99,7 +109,7 @@ export type TQueueSecretScanningDataSourceFullScan = {
scanId: string;
};
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan;
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitBucketResourceDiffScan;
export type TQueueSecretScanningSendNotification = {
dataSource: TSecretScanningDataSources;

View File

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

View File

@@ -0,0 +1,122 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { MultiValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import {
TBitBucketRepo,
useBitBucketConnectionListRepositories
} from "@app/hooks/api/appConnections/bitbucket";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
enum ScanMethod {
AllRepositories = "all-repositories",
SelectRepositories = "select-repositories"
}
export const BitBucketDataSourceConfigFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.BitBucket;
}
>();
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const { data: repositories, isPending: areRepositoriesLoading } =
useBitBucketConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) });
const includeRepos = watch("config.includeRepos");
const scanMethod =
!includeRepos || includeRepos[0] === "*"
? ScanMethod.AllRepositories
: ScanMethod.SelectRepositories;
useEffect(() => {
if (!includeRepos) {
setValue("config.includeRepos", ["*"]);
}
}, [includeRepos, setValue]);
return (
<>
<SecretScanningDataSourceConnectionField
isUpdate={isUpdate}
onChange={() => {
if (scanMethod === ScanMethod.SelectRepositories) {
setValue("config.includeRepos", []);
}
}}
/>
<FormControl label="Scan Repositories">
<Select
value={scanMethod}
onValueChange={(val) => {
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!connectionId}
>
{Object.values(ScanMethod).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
{scanMethod === ScanMethod.SelectRepositories && (
<Controller
name="config.includeRepos"
defaultValue={["*"]}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Include Repositories"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the repository you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={areRepositoriesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
isMulti
value={repositories?.filter((repository) => value.includes(repository.name))}
onChange={(newValue) => {
onChange(
newValue ? (newValue as MultiValue<TBitBucketRepo>).map((p) => p.name) : null
);
}}
options={repositories}
placeholder="Select repositories..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
);
};

View File

@@ -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, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
[SecretScanningDataSource.BitBucket]: BitBucketDataSourceConfigFields
};
export const SecretScanningDataSourceConfigFields = () => {

View File

@@ -0,0 +1,27 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
export const BitBucketDataSourceReviewFields = () => {
const { watch } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.BitBucket;
}
>();
const [{ includeRepos }, connection] = watch(["config", "connection"]);
const shouldScanAll = includeRepos[0] === "*";
return (
<SecretScanningDataSourceConfigReviewSection>
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
<GenericFieldLabel label="Scan Repositories">
{shouldScanAll ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
</SecretScanningDataSourceConfigReviewSection>
);
};

View File

@@ -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, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
[SecretScanningDataSource.BitBucket]: BitBucketDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {

View File

@@ -0,0 +1,18 @@
import { z } from "zod";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
export const BitBucketDataSourceSchema = z
.object({
type: z.literal(SecretScanningDataSource.BitBucket),
config: z.object({
includeRepos: z
.string()
.array()
.min(1, "One or more repositories required")
.max(100, "Cannot configure more than 100 repositories")
})
})
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));

View File

@@ -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<typeof SecretScanningDataSourceSchema>;

View File

@@ -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"
}
};

View File

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

View File

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

View File

@@ -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[];

View File

@@ -1,41 +1,37 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from '@app/components/v2'
import { workspaceKeys } from '@app/hooks/api'
import {
fetchUserProjectPermissions,
roleQueryKeys,
} from '@app/hooks/api/roles/queries'
import { fetchWorkspaceById } from '@app/hooks/api/workspace/queries'
import { ProjectLayout } from '@app/layouts/ProjectLayout'
import { ProjectSelect } from '@app/layouts/ProjectLayout/components/ProjectSelect'
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout',
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId),
})
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId,
workspaceId: params.projectId
}),
queryFn: () =>
fetchUserProjectPermissions({ workspaceId: params.projectId }),
})
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
project,
breadcrumbs: [
{
type: BreadcrumbTypes.Component,
component: ProjectSelect,
},
],
}
},
})
component: ProjectSelect
}
]
};
}
});

View File

@@ -1,14 +1,14 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createFileRoute, redirect } from "@tanstack/react-router";
// this is done as part of migration for multi product inside project
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval',
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval"
)({
beforeLoad: ({ params, search }) => {
throw redirect({
to: '/projects/$projectId/secret-manager/approval',
to: "/projects/$projectId/secret-manager/approval",
params,
search,
})
},
})
search
});
}
});

View File

@@ -0,0 +1,18 @@
import { GenericFieldLabel } from "@app/components/v2";
import { TBitBucketDataSource } from "@app/hooks/api/secretScanningV2/types/bitbucket-data-source";
type Props = {
dataSource: TBitBucketDataSource;
};
export const BitBucketDataSourceConfigDisplay = ({ dataSource }: Props) => {
const {
config: { includeRepos }
} = dataSource;
return (
<GenericFieldLabel label="Scan Repositories">
{includeRepos.includes("*") ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
);
};

View File

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