feature: gitlab secret scanning

This commit is contained in:
Scott Wilson
2025-07-28 15:03:23 -07:00
parent 7d1bc86702
commit f2b0e4ae37
68 changed files with 1711 additions and 94 deletions

View File

@@ -0,0 +1,16 @@
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
import {
CreateGitLabDataSourceSchema,
GitLabDataSourceSchema,
UpdateGitLabDataSourceSchema
} from "@app/ee/services/secret-scanning-v2/gitlab";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export const registerGitLabSecretScanningRouter = async (server: FastifyZodProvider) =>
registerSecretScanningEndpoints({
type: SecretScanningDataSource.GitLab,
server,
responseSchema: GitLabDataSourceSchema,
createSchema: CreateGitLabDataSourceSchema,
updateSchema: UpdateGitLabDataSourceSchema
});

View File

@@ -1,3 +1,4 @@
import { registerGitLabSecretScanningRouter } from "@app/ee/routes/v2/secret-scanning-v2-routers/gitlab-secret-scanning-router";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
@@ -10,5 +11,6 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
(server: FastifyZodProvider) => Promise<void>
> = {
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter,
[SecretScanningDataSource.GitLab]: registerGitLabSecretScanningRouter
};

View File

@@ -4,6 +4,7 @@ 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 { GitLabDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
import {
SecretScanningFindingStatus,
SecretScanningScanStatus
@@ -24,7 +25,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
GitHubDataSourceListItemSchema,
BitbucketDataSourceListItemSchema
BitbucketDataSourceListItemSchema,
GitLabDataSourceListItemSchema
]);
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {

View File

@@ -18,7 +18,8 @@ import {
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization,
TSecretScanningFactoryTeardown
TSecretScanningFactoryTeardown,
TSecretScanningFactoryValidateConfigUpdate
} 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";
@@ -302,6 +303,13 @@ export const BitbucketSecretScanningFactory = () => {
);
};
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
TBitbucketDataSourceInput["config"],
TBitbucketDataSourceWithConnection
> = async () => {
// no validation required
};
return {
initialize,
postInitialization,
@@ -309,6 +317,7 @@ export const BitbucketSecretScanningFactory = () => {
getFullScanPath,
getDiffScanResourcePayload,
getDiffScanFindingsPayload,
teardown
teardown,
validateConfigUpdate
};
};

View File

@@ -20,7 +20,8 @@ import {
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization,
TSecretScanningFactoryTeardown
TSecretScanningFactoryTeardown,
TSecretScanningFactoryValidateConfigUpdate
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
@@ -64,7 +65,14 @@ export const GitHubSecretScanningFactory = () => {
};
const teardown: TSecretScanningFactoryTeardown<TGitHubDataSourceWithConnection> = async () => {
// no termination required
// no teardown required
};
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
TGitHubDataSourceInput["config"],
TGitHubDataSourceWithConnection
> = async () => {
// no validation required
};
const listRawResources: TSecretScanningFactoryListRawResources<TGitHubDataSourceWithConnection> = async (
@@ -238,6 +246,7 @@ export const GitHubSecretScanningFactory = () => {
getFullScanPath,
getDiffScanResourcePayload,
getDiffScanFindingsPayload,
teardown
teardown,
validateConfigUpdate
};
};

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 GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = {
name: "GitLab",
type: SecretScanningDataSource.GitLab,
connection: AppConnection.GitLab
};

View File

@@ -0,0 +1,8 @@
export enum GitLabDataSourceScope {
Project = "project",
Group = "group"
}
export enum GitLabWebHookEvent {
Push = "Push Hook"
}

View File

@@ -0,0 +1,409 @@
import { Camelize, GitbeakerRequestError, GroupHookSchema, ProjectHookSchema } from "@gitbeaker/rest";
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,
TSecretScanningFactoryParams,
TSecretScanningFactoryPostInitialization,
TSecretScanningFactoryTeardown,
TSecretScanningFactoryValidateConfigUpdate
} 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 { alphaNumericNanoId } from "@app/lib/nanoid";
import { GitLabProjectRegex } from "@app/lib/regex";
import {
getGitLabConnectionClient,
getGitLabInstanceUrl,
TGitLabConnection
} from "@app/services/app-connection/gitlab";
import { GitLabDataSourceScope } from "./gitlab-secret-scanning-enums";
import {
TGitLabDataSourceCredentials,
TGitLabDataSourceInput,
TGitLabDataSourceWithConnection,
TQueueGitLabResourceDiffScan
} from "./gitlab-secret-scanning-types";
const getMainDomain = (instanceUrl: string) => {
const url = new URL(instanceUrl);
const { hostname } = url;
const parts = hostname.split(".");
if (parts.length >= 2) {
return parts.slice(-2).join(".");
}
return hostname;
};
export const GitLabSecretScanningFactory = ({ appConnectionDAL, kmsService }: TSecretScanningFactoryParams) => {
const initialize: TSecretScanningFactoryInitialize<
TGitLabDataSourceInput,
TGitLabConnection,
TGitLabDataSourceCredentials
> = async ({ payload: { config, name }, connection }, callback) => {
const token = alphaNumericNanoId(64);
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const appCfg = getConfig();
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
const project = await client.Projects.show(projectId);
if (!project) {
throw new BadRequestError({ message: `Could not find project with ID ${projectId}.` });
}
let hook: Camelize<ProjectHookSchema>;
try {
hook = await client.ProjectHooks.add(projectId, `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`, {
token,
pushEvents: true,
enableSslVerification: true,
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${name}`
});
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new BadRequestError({ message: `${error.message}: ${error.cause?.description ?? "Unknown Error"}` });
}
throw error;
}
try {
return await callback({
credentials: {
token,
hookId: hook.id
}
});
} catch (error) {
try {
await client.ProjectHooks.remove(projectId, hook.id);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
}
// group scope
const { groupId } = config;
const group = await client.Groups.show(groupId);
if (!group) {
throw new BadRequestError({ message: `Could not find group with ID ${groupId}.` });
}
let hook: Camelize<GroupHookSchema>;
try {
hook = await client.GroupHooks.add(groupId, `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`, {
token,
pushEvents: true,
enableSslVerification: true,
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${name}`
});
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new BadRequestError({ message: error.message });
}
throw error;
}
try {
return await callback({
credentials: {
token,
hookId: hook.id
}
});
} catch (error) {
try {
await client.GroupHooks.remove(groupId, hook.id);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
};
const postInitialization: TSecretScanningFactoryPostInitialization<
TGitLabDataSourceInput,
TGitLabConnection,
TGitLabDataSourceCredentials
> = async ({ connection, dataSourceId, credentials, payload: { config } }) => {
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const appCfg = getConfig();
const hookUrl = `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`;
const { hookId } = credentials;
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
try {
await client.ProjectHooks.edit(projectId, hookId, hookUrl, {
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${dataSourceId}`,
custom_headers: [{ key: "x-data-source-id", value: dataSourceId }]
});
} catch (error) {
try {
await client.ProjectHooks.remove(projectId, hookId);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
return;
}
// group-scope
const { groupId } = config;
try {
await client.GroupHooks.edit(groupId, hookId, hookUrl, {
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${dataSourceId}`,
custom_headers: [{ key: "x-data-source-id", value: dataSourceId }]
});
} catch (error) {
try {
await client.GroupHooks.remove(groupId, hookId);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
};
const listRawResources: TSecretScanningFactoryListRawResources<TGitLabDataSourceWithConnection> = async (
dataSource
) => {
const { connection, config } = dataSource;
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
const project = await client.Projects.show(projectId);
if (!project) {
throw new BadRequestError({ message: `Could not find project with ID ${projectId}.` });
}
// scott: even though we have this data we want to get potentially updated name
return [
{
name: project.pathWithNamespace,
externalId: project.id.toString(),
type: SecretScanningResource.Project
}
];
}
// group-scope
const { groupId, includeProjects } = config;
const projects = await client.Groups.allProjects(groupId, {
archived: false
});
const filteredProjects: typeof projects = [];
if (!includeProjects || includeProjects.includes("*")) {
filteredProjects.push(...projects);
} else {
filteredProjects.push(...projects.filter((project) => includeProjects.includes(project.pathWithNamespace)));
}
return filteredProjects.map(({ id, pathWithNamespace }) => ({
name: pathWithNamespace,
externalId: id.toString(),
type: SecretScanningResource.Project
}));
};
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TGitLabDataSourceWithConnection> = async ({
dataSource,
resourceName,
tempFolder
}) => {
const { connection } = dataSource;
const instanceUrl = await getGitLabInstanceUrl(connection.credentials.instanceUrl);
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const user = await client.Users.showCurrentUser();
const repoPath = join(tempFolder, "repo.git");
if (!GitLabProjectRegex.test(resourceName)) {
throw new Error("Invalid GitLab project name");
}
await cloneRepository({
cloneUrl: `https://${user.username}:${connection.credentials.accessToken}@${getMainDomain(instanceUrl)}/${resourceName}.git`,
repoPath
});
return repoPath;
};
const teardown: TSecretScanningFactoryTeardown<
TGitLabDataSourceWithConnection,
TGitLabDataSourceCredentials
> = async ({ dataSource: { connection, config }, credentials: { hookId } }) => {
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
try {
await client.ProjectHooks.remove(projectId, hookId);
} catch (error) {
// do nothing, just try to clean up webhook
}
return;
}
const { groupId } = config;
try {
await client.GroupHooks.remove(groupId, hookId);
} catch (error) {
// do nothing, just try to clean up webhook
}
};
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
TQueueGitLabResourceDiffScan["payload"]
> = ({ project }) => {
return {
name: project.path_with_namespace,
externalId: project.id.toString(),
type: SecretScanningResource.Project
};
};
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
TGitLabDataSourceWithConnection,
TQueueGitLabResourceDiffScan["payload"]
> = async ({ dataSource, payload, resourceName, configPath }) => {
const { connection } = dataSource;
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const { commits, project } = payload;
const allFindings: SecretMatch[] = [];
for (const commit of commits) {
// eslint-disable-next-line no-await-in-loop
const commitDiffs = await client.Commits.showDiff(project.id, commit.id);
for (const commitDiff of commitDiffs) {
// eslint-disable-next-line no-continue
if (commitDiff.deletedFile) continue;
// eslint-disable-next-line no-await-in-loop
const findings = await scanContentAndGetFindings(
replaceNonChangesWithNewlines(`\n${commitDiff.diff}`),
configPath
);
const adjustedFindings = findings.map((finding) => {
const startLine = convertPatchLineToFileLineNumber(commitDiff.diff, finding.StartLine);
const endLine =
finding.StartLine === finding.EndLine
? startLine
: convertPatchLineToFileLineNumber(commitDiff.diff, finding.EndLine);
const startColumn = finding.StartColumn - 1; // subtract 1 for +
const endColumn = finding.EndColumn - 1; // subtract 1 for +
const authorName = commit.author.name;
const authorEmail = commit.author.email;
return {
...finding,
StartLine: startLine,
EndLine: endLine,
StartColumn: startColumn,
EndColumn: endColumn,
File: commitDiff.newPath,
Commit: commit.id,
Author: authorName,
Email: authorEmail,
Message: commit.message,
Fingerprint: `${commit.id}:${commitDiff.newPath}:${finding.RuleID}:${startLine}:${startColumn}`,
Date: commit.timestamp,
Link: `https://gitlab.com/${resourceName}/blob/${commit.id}/${commitDiff.newPath}#L${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
})
);
};
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
TGitLabDataSourceInput["config"],
TGitLabDataSourceWithConnection
> = async ({ config, dataSource }) => {
if (dataSource.config.scope !== config.scope) {
throw new BadRequestError({ message: "Cannot change Data Source scope after creation." });
}
};
return {
listRawResources,
getFullScanPath,
initialize,
postInitialization,
teardown,
getDiffScanResourcePayload,
getDiffScanFindingsPayload,
validateConfigUpdate
};
};

View File

@@ -0,0 +1,101 @@
import { z } from "zod";
import { GitLabDataSourceScope } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
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 { GitLabProjectRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const GitLabDataSourceConfigSchema = z.discriminatedUnion("scope", [
z.object({
scope: z.literal(GitLabDataSourceScope.Group).describe(SecretScanningDataSources.CONFIG.GITLAB.scope),
groupId: z.number().describe(SecretScanningDataSources.CONFIG.GITLAB.groupId),
groupName: z.string().trim().max(256).optional().describe(SecretScanningDataSources.CONFIG.GITLAB.groupName),
includeProjects: z
.array(
z
.string()
.min(1)
.max(256)
.refine((value) => value === "*" || GitLabProjectRegex.test(value), "Invalid project name format")
)
.nonempty("One or more projects required")
.max(100, "Cannot configure more than 100 projects")
.default(["*"])
.describe(SecretScanningDataSources.CONFIG.GITLAB.includeProjects)
}),
z.object({
scope: z.literal(GitLabDataSourceScope.Project).describe(SecretScanningDataSources.CONFIG.GITLAB.scope),
projectName: z.string().trim().max(256).optional().describe(SecretScanningDataSources.CONFIG.GITLAB.projectName),
projectId: z.number().describe(SecretScanningDataSources.CONFIG.GITLAB.projectId)
})
]);
export const GitLabDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.GitLab,
isConnectionRequired: true
})
.extend({
config: GitLabDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const CreateGitLabDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.GitLab,
isConnectionRequired: true
})
.extend({
config: GitLabDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const UpdateGitLabDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(SecretScanningDataSource.GitLab)
.extend({
config: GitLabDataSourceConfigSchema.optional()
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const GitLabDataSourceListItemSchema = z
.object({
name: z.literal("GitLab"),
connection: z.literal(AppConnection.GitLab),
type: z.literal(SecretScanningDataSource.GitLab)
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const GitLabFindingSchema = BaseSecretScanningFindingSchema.extend({
resourceType: z.literal(SecretScanningResource.Project),
dataSourceType: z.literal(SecretScanningDataSource.GitLab),
details: GitRepositoryScanFindingDetailsSchema
});
export const GitLabDataSourceCredentialsSchema = z.object({
token: z.string(),
hookId: z.number()
});

View File

@@ -0,0 +1,94 @@
import { GitLabDataSourceScope } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
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 {
TGitLabDataSource,
TGitLabDataSourceCredentials,
THandleGitLabPushEvent
} from "./gitlab-secret-scanning-types";
export const gitlabSecretScanningService = (
secretScanningV2DAL: TSecretScanningV2DALFactory,
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const handlePushEvent = async ({ payload, token, dataSourceId }: THandleGitLabPushEvent) => {
if (!payload.total_commits_count || !payload.project) {
logger.warn(
`secretScanningV2PushEvent: GitLab - Insufficient data [changes=${
payload.total_commits_count ?? 0
}] [projectName=${payload.project.path_with_namespace}] [projectId=${payload.project.id}]`
);
return;
}
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
id: dataSourceId,
type: SecretScanningDataSource.GitLab
})) as TGitLabDataSource | undefined;
if (!dataSource) {
logger.error(
`secretScanningV2PushEvent: GitLab - Could not find data source [dataSourceId=${dataSourceId}] [projectId=${payload.project.id}]`
);
return;
}
const { isAutoScanEnabled, config, encryptedCredentials, projectId } = dataSource;
if (!encryptedCredentials) {
logger.info(
`secretScanningV2PushEvent: GitLab - Could not find encrypted credentials [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
return;
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedCredentials = decryptor({ cipherTextBlob: encryptedCredentials });
const credentials = JSON.parse(decryptedCredentials.toString()) as TGitLabDataSourceCredentials;
if (token !== credentials.token) {
logger.error(
`secretScanningV2PushEvent: GitLab - Invalid webhook token [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
return;
}
if (!isAutoScanEnabled) {
logger.info(
`secretScanningV2PushEvent: GitLab - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
return;
}
if (
config.scope === GitLabDataSourceScope.Project
? config.projectId.toString() === payload.project_id.toString()
: config.includeProjects.includes("*") || config.includeProjects.includes(payload.project.path_with_namespace)
) {
await secretScanningV2Queue.queueResourceDiffScan({
dataSourceType: SecretScanningDataSource.GitLab,
payload,
dataSourceId: dataSource.id
});
} else {
logger.info(
`secretScanningV2PushEvent: GitLab - ignoring due to repository not being present in config [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
}
};
return {
handlePushEvent
};
};

View File

@@ -0,0 +1,97 @@
import { z } from "zod";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TGitLabConnection } from "@app/services/app-connection/gitlab";
import {
CreateGitLabDataSourceSchema,
GitLabDataSourceCredentialsSchema,
GitLabDataSourceListItemSchema,
GitLabDataSourceSchema,
GitLabFindingSchema
} from "./gitlab-secret-scanning-schemas";
export type TGitLabDataSource = z.infer<typeof GitLabDataSourceSchema>;
export type TGitLabDataSourceInput = z.infer<typeof CreateGitLabDataSourceSchema>;
export type TGitLabDataSourceListItem = z.infer<typeof GitLabDataSourceListItemSchema>;
export type TGitLabFinding = z.infer<typeof GitLabFindingSchema>;
export type TGitLabDataSourceWithConnection = TGitLabDataSource & {
connection: TGitLabConnection;
};
export type TGitLabDataSourceCredentials = z.infer<typeof GitLabDataSourceCredentialsSchema>;
export type TGitLabDataSourcePushEventPayload = {
object_kind: "push";
event_name: "push";
before: string;
after: string;
ref: string;
ref_protected: boolean;
checkout_sha: string;
user_id: number;
user_name: string;
user_username: string;
user_email: string;
user_avatar: string;
project_id: number;
project: {
id: number;
name: string;
description: string;
web_url: string;
avatar_url: string | null;
git_ssh_url: string;
git_http_url: string;
namespace: string;
visibility_level: number;
path_with_namespace: string;
default_branch: string;
homepage: string;
url: string;
ssh_url: string;
http_url: string;
};
repository: {
name: string;
url: string;
description: string;
homepage: string;
git_http_url: string;
git_ssh_url: string;
visibility_level: number;
};
commits: {
id: string;
message: string;
title: string;
timestamp: string;
url: string;
author: {
name: string;
email: string;
};
added: string[];
modified: string[];
removed: string[];
}[];
total_commits_count: number;
};
export type THandleGitLabPushEvent = {
payload: TGitLabDataSourcePushEventPayload;
dataSourceId: string;
token: string;
};
export type TQueueGitLabResourceDiffScan = {
dataSourceType: SecretScanningDataSource.GitLab;
payload: TGitLabDataSourcePushEventPayload;
dataSourceId: string;
resourceId: string;
scanId: string;
};

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanni
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 { GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/gitlab";
import { titleCaseToCamelCase } from "@app/lib/fn";
import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
@@ -13,7 +14,8 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte
const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = {
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
[SecretScanningDataSource.GitLab]: GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
};
export const listSecretScanningDataSourceOptions = () => {

View File

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

View File

@@ -16,6 +16,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
@@ -48,6 +49,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
};
@@ -62,7 +64,8 @@ export const secretScanningV2QueueServiceFactory = async ({
smtpService,
kmsService,
auditLogService,
keyStore
keyStore,
appConnectionDAL
}: TSecretRotationV2QueueServiceFactoryDep) => {
const queueDataSourceFullScan = async (
dataSource: TSecretScanningDataSourceWithConnection,
@@ -71,7 +74,10 @@ export const secretScanningV2QueueServiceFactory = async ({
try {
const { type } = dataSource;
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
const factory = SECRET_SCANNING_FACTORY_MAP[type]({
kmsService,
appConnectionDAL
});
const rawResources = await factory.listRawResources(dataSource);
@@ -171,7 +177,10 @@ export const secretScanningV2QueueServiceFactory = async ({
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const findingsPath = join(tempFolder, "findings.json");
@@ -329,7 +338,10 @@ export const secretScanningV2QueueServiceFactory = async ({
dataSourceId,
dataSourceType
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "dataSourceId" | "dataSourceType">) => {
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]();
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const resourcePayload = factory.getDiffScanResourcePayload(payload);
@@ -391,7 +403,10 @@ export const secretScanningV2QueueServiceFactory = async ({
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const tempFolder = await createTempFolder();

View File

@@ -46,6 +46,7 @@ import {
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
@@ -53,12 +54,14 @@ 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 { gitlabSecretScanningService } from "./gitlab/gitlab-secret-scanning-service";
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
export type TSecretScanningV2ServiceFactoryDep = {
secretScanningV2DAL: TSecretScanningV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretScanningV2Queue: Pick<
@@ -76,6 +79,7 @@ export const secretScanningV2ServiceFactory = ({
appConnectionService,
licenseService,
secretScanningV2Queue,
appConnectionDAL,
kmsService
}: TSecretScanningV2ServiceFactoryDep) => {
const $checkListSecretScanningDataSourcesByProjectIdPermissions = async (
@@ -255,7 +259,10 @@ export const secretScanningV2ServiceFactory = ({
);
}
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]();
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]({
appConnectionDAL,
kmsService
});
try {
const createdDataSource = await factory.initialize(
@@ -363,6 +370,31 @@ export const secretScanningV2ServiceFactory = ({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
let connection: TAppConnection | null = null;
if (dataSource.connectionId) {
// validates permission to connect and app is valid for data source
connection = await appConnectionService.connectAppConnectionById(
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type],
dataSource.connectionId,
actor
);
}
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type]({
appConnectionDAL,
kmsService
});
if (payload.config) {
await factory.validateConfigUpdate({
dataSource: {
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
config: payload.config as TSecretScanningDataSourceWithConnection["config"]
});
}
try {
const updatedDataSource = await secretScanningV2DAL.dataSources.updateById(dataSourceId, payload);
@@ -416,7 +448,10 @@ export const secretScanningV2ServiceFactory = ({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
const factory = SECRET_SCANNING_FACTORY_MAP[type]({
appConnectionDAL,
kmsService
});
let connection: TAppConnection | null = null;
if (dataSource.connection) {
@@ -903,6 +938,7 @@ export const secretScanningV2ServiceFactory = ({
findSecretScanningConfigByProjectId,
upsertSecretScanningConfig,
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService),
gitlab: gitlabSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
};
};

View File

@@ -21,14 +21,25 @@ import {
TGitHubFinding,
TQueueGitHubResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/github";
import {
TGitLabDataSource,
TGitLabDataSourceCredentials,
TGitLabDataSourceInput,
TGitLabDataSourceListItem,
TGitLabDataSourceWithConnection,
TGitLabFinding,
TQueueGitLabResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/gitlab";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource | TGitLabDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt?: Date | null;
@@ -52,15 +63,25 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
export type TSecretScanningDataSourceWithConnection =
| TGitHubDataSourceWithConnection
| TBitbucketDataSourceWithConnection;
| TBitbucketDataSourceWithConnection
| TGitLabDataSourceWithConnection;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitbucketDataSourceInput;
export type TSecretScanningDataSourceInput =
| TGitHubDataSourceInput
| TBitbucketDataSourceInput
| TGitLabDataSourceInput;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitbucketDataSourceListItem;
export type TSecretScanningDataSourceListItem =
| TGitHubDataSourceListItem
| TBitbucketDataSourceListItem
| TGitLabDataSourceListItem;
export type TSecretScanningDataSourceCredentials = TBitbucketDataSourceCredentials | undefined;
export type TSecretScanningDataSourceCredentials =
| TBitbucketDataSourceCredentials
| TGitLabDataSourceCredentials
| undefined;
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding;
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding | TGitLabFinding;
export type TListSecretScanningDataSourcesByProjectId = {
projectId: string;
@@ -112,7 +133,10 @@ export type TQueueSecretScanningDataSourceFullScan = {
scanId: string;
};
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan;
export type TQueueSecretScanningResourceDiffScan =
| TQueueGitHubResourceDiffScan
| TQueueBitbucketResourceDiffScan
| TQueueGitLabResourceDiffScan;
export type TQueueSecretScanningSendNotification = {
dataSource: TSecretScanningDataSources;
@@ -170,6 +194,11 @@ export type TSecretScanningFactoryInitialize<
callback: (parameters: { credentials?: C; externalId?: string }) => Promise<TSecretScanningDataSourceRaw>
) => Promise<TSecretScanningDataSourceRaw>;
export type TSecretScanningFactoryValidateConfigUpdate<
C extends TSecretScanningDataSourceInput["config"],
T extends TSecretScanningDataSourceWithConnection
> = (params: { config: C; dataSource: T }) => Promise<void>;
export type TSecretScanningFactoryPostInitialization<
P extends TSecretScanningDataSourceInput,
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
@@ -181,17 +210,23 @@ export type TSecretScanningFactoryTeardown<
C extends TSecretScanningDataSourceCredentials = undefined
> = (params: { dataSource: T; credentials: C }) => Promise<void>;
export type TSecretScanningFactoryParams = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSecretScanningFactory<
T extends TSecretScanningDataSourceWithConnection,
P extends TQueueSecretScanningResourceDiffScan["payload"],
I extends TSecretScanningDataSourceInput,
C extends TSecretScanningDataSourceCredentials | undefined = undefined
> = () => {
> = (params: TSecretScanningFactoryParams) => {
listRawResources: TSecretScanningFactoryListRawResources<T>;
getFullScanPath: TSecretScanningFactoryGetFullScanPath<T>;
initialize: TSecretScanningFactoryInitialize<I, T["connection"] | undefined, C>;
postInitialization: TSecretScanningFactoryPostInitialization<I, T["connection"] | undefined, C>;
teardown: TSecretScanningFactoryTeardown<T, C>;
validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<I["config"], T>;
getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<P>;
getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<T, P>;
};

View File

@@ -2,10 +2,12 @@ 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";
import { GitLabDataSourceSchema, GitLabFindingSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema,
BitbucketDataSourceSchema
BitbucketDataSourceSchema,
GitLabDataSourceSchema
]);
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
@@ -18,5 +20,10 @@ export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType"
JSON.stringify({
title: "Bitbucket"
})
),
GitLabFindingSchema.describe(
JSON.stringify({
title: "GitLab"
})
)
]);

View File

@@ -2701,6 +2701,14 @@ export const SecretScanningDataSources = {
GITHUB: {
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
},
GITLAB: {
includeProjects: 'The projects to include when scanning. Defaults to all projects (["*"]).',
scope: "The GitLab scope scanning should occur at (project or group level).",
projectId: "The ID of the project to scan.",
projectName: "The name of the project to scan.",
groupId: "The ID of the group to scan projects from.",
groupName: "The name of the group to scan projects from."
},
BITBUCKET: {
workspaceSlug: "The workspace to scan.",
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'

View File

@@ -11,3 +11,5 @@ export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-]
export const LdapUrlRegex = new RE2(/^ldaps?:\/\//);
export const BasicRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/);
export const GitLabProjectRegex = new RE2(/^[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)+$/);

View File

@@ -4,6 +4,8 @@ import { Probot } from "probot";
import { z } from "zod";
import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types";
import { TGitLabDataSourcePushEventPayload } from "@app/ee/services/secret-scanning-v2/gitlab";
import { GitLabWebHookEvent } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -113,4 +115,36 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
return res.send("ok");
}
});
// gitlab push event webhook
server.route({
method: "POST",
url: "/gitlab",
config: {
rateLimit: writeLimit
},
handler: async (req, res) => {
const event = req.headers["x-gitlab-event"] as GitLabWebHookEvent;
const token = req.headers["x-gitlab-token"] as string;
const dataSourceId = req.headers["x-data-source-id"] as string;
if (event !== GitLabWebHookEvent.Push) {
return res.status(400).send({ message: `Event type not supported: ${event as string}` });
}
if (!token) {
return res.status(401).send({ message: "Unauthorized: Missing token" });
}
if (!dataSourceId) return res.status(400).send({ message: "Data Source ID header is required" });
await server.services.secretScanningV2.gitlab.handlePushEvent({
dataSourceId,
payload: req.body as TGitLabDataSourcePushEventPayload,
token
});
return res.send("ok");
}
});
};

View File

@@ -1931,7 +1931,8 @@ export const registerRoutes = async (
projectMembershipDAL,
smtpService,
kmsService,
keyStore
keyStore,
appConnectionDAL
});
const secretScanningV2Service = secretScanningV2ServiceFactory({
@@ -1940,7 +1941,8 @@ export const registerRoutes = async (
licenseService,
secretScanningV2DAL,
secretScanningV2Queue,
kmsService
kmsService,
appConnectionDAL
});
// setup the communication with license key server

View File

@@ -222,6 +222,37 @@ export const validateGitLabConnectionCredentials = async (config: TGitLabConnect
return inputCredentials;
};
export const getGitLabConnectionClient = async (
appConnection: TGitLabConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
let { accessToken } = appConnection.credentials;
if (
appConnection.method === GitLabConnectionMethod.OAuth &&
appConnection.credentials.refreshToken &&
new Date(appConnection.credentials.expiresAt) < new Date()
) {
accessToken = await refreshGitLabToken(
appConnection.credentials.refreshToken,
appConnection.id,
appConnection.orgId,
appConnectionDAL,
kmsService,
appConnection.credentials.instanceUrl
);
}
const client = await getGitLabClient(
accessToken,
appConnection.credentials.instanceUrl,
appConnection.method === GitLabConnectionMethod.OAuth
);
return client;
};
export const listGitLabProjects = async ({
appConnection,
appConnectionDAL,

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/data-source-name/{dataSourceName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List Resources"
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/resources"
---

View File

@@ -0,0 +1,4 @@
---
title: "List Scans"
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/scans"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab"
---

View File

@@ -0,0 +1,4 @@
---
title: "Scan Resource"
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/resources/{resourceId}/scan"
---

View File

@@ -0,0 +1,4 @@
---
title: "Scan"
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/scan"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
---

View File

@@ -220,7 +220,8 @@
"pages": [
"documentation/platform/secret-scanning/overview",
"documentation/platform/secret-scanning/bitbucket",
"documentation/platform/secret-scanning/github"
"documentation/platform/secret-scanning/github",
"documentation/platform/secret-scanning/gitlab"
]
}
]
@@ -1199,6 +1200,21 @@
"api-reference/endpoints/secret-scanning/data-sources/github/scan",
"api-reference/endpoints/secret-scanning/data-sources/github/scan-resource"
]
},
{
"group": "GitLab",
"pages": [
"api-reference/endpoints/secret-scanning/data-sources/gitlab/list",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-id",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-name",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/list-resources",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/list-scans",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/create",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/update",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/delete",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/scan",
"api-reference/endpoints/secret-scanning/data-sources/gitlab/scan-resource"
]
}
]
},

View File

@@ -0,0 +1,103 @@
---
title: "GitLab Secret Scanning"
sidebarTitle: "GitLab"
description: "Learn how to configure secret scanning for GitLab."
---
## Prerequisites
- Create a [GitLab Connection](/integrations/app-connections/gitlab) with Secret Scanning permissions
## Create a GitLab Data Source in Infisical
<Tabs>
<Tab title="Infisical UI">
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 **GitLab** option.
![Select GitLab](/images/platform/secret-scanning/gitlab/step-2.png)
3. Configure which workspace and repositories you would like to scan. Then click **Next**.
![Data Source Configuration](/images/platform/secret-scanning/gitlab/step-3.png)
- **GitLab Connection** - the connection that has access to the repositories you want to scan.
- **Scope** - the GitLab scope to scan secrets in.
- **Project** - scan an individual GitLab project.
- **Group** - scan one or more projects belonging to a GitLab group.
- **Scan Repositories** - when using **Group Scope**, 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/gitlab/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/gitlab/step-5.png)
6. Your **GitLab Data Source** is now available and will begin a full scan if **Auto-Scan** is enabled.
![Data Source Created](/images/platform/secret-scanning/gitlab/step-6.png)
7. You can view repositories and scan results by clicking on your data source.
![Data Source Page](/images/platform/secret-scanning/gitlab/step-7.png)
8. In addition, you can review any findings from the **Findings Page**.
![Findings Page](/images/platform/secret-scanning/gitlab/step-8.png)
</Tab>
<Tab title="API">
To create a GitLab Data Source, make an API request to the [Create GitLab Data Source](/api-reference/endpoints/secret-scanning/data-sources/gitlab/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://us.infisical.com/api/v2/secret-scanning/data-sources/gitlab \
--header 'Content-Type: application/json' \
--data '{
"name": "my-gitlab-source",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "my gitlab data source",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"isAutoScanEnabled": true,
"config": {
"scope": "project",
"projectId": 123456789,
"projectName": "my-group/my-project"
}
}'
```
### Sample response
```bash Response
{
"dataSource": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"externalId": "1234567890",
"name": "my-gitlab-source",
"description": "my gitlab data source",
"isAutoScanEnabled": true,
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"type": "gitlab",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": {
"app": "gitlab",
"name": "my-gitlab-app",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"config": {
"scope": "project",
"projectId": 123456789,
"projectName": "my-group/my-project"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -26,8 +26,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/gitlab/oauth/callback`.
![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-top.png)
![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-bottom.png)
Depending on your use case, add one or more of the following scopes to your application:
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, your application will require the `api` scope:
![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-top-secret-sync.png)
![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-bottom-secret-sync.png)
</Tab>
<Tab title="Secret Scanning">
For Secret Scanning, your application will require the `api` and `read_repository` scopes:
![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-top-secret-scanning.png)
![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-bottom-secret-scanning.png)
</Tab>
</Tabs>
<Tip>
The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance.
@@ -96,16 +110,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
![GitLab Personal Access Tokens](/images/app-connections/gitlab/gitlab-add-access-token.png)
</Step>
<Step title="Configure Token">
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, your token will require the ability to access the API:
Fill in the token details:
- **Token name**: A descriptive name for the token (e.g., "connection-token")
- **Expiration date**: Set an appropriate expiration date
- **Select scopes**: Choose the **api** scope for full API access
- **Select scopes**: Depending on your use case, add one or more of the following scopes:
![GitLab Personal Token Form](/images/app-connections/gitlab/gitlab-personal-access-token-form.png)
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, your token will require the `api` scope:
![GitLab Personal Token Form](/images/app-connections/gitlab/gitlab-personal-access-token-form-secret-sync.png)
</Tab>
<Tab title="Secret Scanning">
For Secret Scanning, your token will require the `api` and `read_repository` scopes:
![GitLab Personal Token Form](/images/app-connections/gitlab/gitlab-personal-access-token-form-secret-scanning.png)
</Tab>
</Tabs>
<Info>
@@ -134,17 +154,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
![GitLab Project Access Tokens](/images/app-connections/gitlab/gitlab-project-access-token-list.png)
</Step>
<Step title="Configure Token">
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, your token will require the ability to access the API and be at least an **Owner**:
Fill in the token details:
- **Token name**: A descriptive name for the token
- **Expiration date**: Set an appropriate expiration date
- **Select role**: Choose **Owner** or higher role
- **Select scopes**: Choose the **api** scope for API access
Fill in the token details:
- **Token name**: A descriptive name for the token
- **Expiration date**: Set an appropriate expiration date
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
![GitLab Create Project Token](/images/app-connections/gitlab/gitlab-project-access-token-form.png)
</Tab>
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, your token will require the `api` scope and at least the **Owner** role:
![GitLab Create Project Token](/images/app-connections/gitlab/gitlab-project-access-token-form-secret-sync.png)
</Tab>
<Tab title="Secret Scanning">
For Secret Scanning, your token will require the `api` and `read_repository` scopes and the **Maintainer** role:
![GitLab Create Project Token](/images/app-connections/gitlab/gitlab-project-access-token-form-secret-scanning.png)
</Tab>
</Tabs>
<Info>

View File

@@ -0,0 +1,274 @@
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 {
TGitLabGroup,
TGitLabProject,
useGitLabConnectionListGroups,
useGitLabConnectionListProjects
} from "@app/hooks/api/appConnections/gitlab";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
enum ScanMethod {
AllProjects = "all-projects",
SelectProjects = "select-projects"
}
export const GitLabDataSourceConfigFields = () => {
const { control, watch, setValue, formState } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitLab;
}
>();
console.log(formState);
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const scope = watch("config.scope");
const groupName = watch("config.groupName");
const includeProjects = watch("config.includeProjects");
const { data: projects, isPending: isProjectsPending } = useGitLabConnectionListProjects(
connectionId,
{ enabled: Boolean(connectionId) }
);
const { data: groups, isPending: isGroupsPending } = useGitLabConnectionListGroups(connectionId, {
enabled: Boolean(connectionId) && scope === GitLabDataSourceScope.Group
});
const scanMethod =
!includeProjects || includeProjects.includes("*")
? ScanMethod.AllProjects
: ScanMethod.SelectProjects;
useEffect(() => {
if (!includeProjects) {
setValue("config.includeProjects", ["*"]);
}
}, [includeProjects]);
const clearAllFields = () => {
setValue("config.includeProjects", []);
setValue("config.projectName", "");
setValue("config.groupName", "");
// @ts-expect-error rhf doesn't like this but we need to reset
setValue("config.projectId", undefined);
// @ts-expect-error rhf doesn't like this but we need to reset
setValue("config.groupId", undefined);
};
return (
<>
<SecretScanningDataSourceConnectionField isUpdate={isUpdate} onChange={clearAllFields} />
<Controller
name="config.scope"
control={control}
defaultValue={GitLabDataSourceScope.Project}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Scope"
helperText={isUpdate ? "Cannot be updated" : undefined}
tooltipText={
<div className="flex flex-col gap-3">
<p>Specify the GitLab scope scanning should be performed at:</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">Project</span>: Scan an
individual GitLab project.
</p>
</li>
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">Group</span>: Scan one or more
projects belonging to a GitLab group.
</p>
</li>
</ul>
</div>
}
>
<Select
value={value}
onValueChange={(v) => {
onChange(v);
clearAllFields();
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
isDisabled={isUpdate}
dropdownContainerClassName="max-w-none"
>
{Object.values(GitLabDataSourceScope).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
{scope === GitLabDataSourceScope.Project ? (
<Controller
name="config.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
isLoading={isProjectsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects?.find((project) => value === Number.parseInt(project.id, 10))}
onChange={(newValue) => {
const project = newValue as SingleValue<TGitLabProject>;
onChange(project ? Number.parseInt(project.id, 10) : null);
setValue("config.projectName", project?.name ?? "");
}}
options={projects}
placeholder="Select project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
) : (
<>
<Controller
name="config.groupId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Group"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the group you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
isLoading={isGroupsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={groups?.find((group) => value === Number.parseInt(group.id, 10))}
onChange={(newValue) => {
const group = newValue as SingleValue<TGitLabGroup>;
onChange(group ? Number.parseInt(group.id, 10) : null);
setValue("config.groupName", group?.name ?? "");
setValue("config.includeProjects", ["*"]);
}}
options={groups}
placeholder="Select group..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
<FormControl label="Scan Projects">
<Select
value={scanMethod}
onValueChange={(val) => {
setValue("config.includeProjects", val === ScanMethod.AllProjects ? ["*"] : []);
}}
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.SelectProjects && (
<Controller
name="config.includeProjects"
defaultValue={["*"]}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Include Projects"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsPending && Boolean(connectionId)}
isDisabled={!connectionId || !groupName}
isMulti
value={projects?.filter((project) => value.includes(project.name))}
onChange={(newValue) => {
onChange(
newValue
? (newValue as MultiValue<TGitLabProject>).map((p) => p.name)
: null
);
}}
options={projects?.filter((project) => project.name.startsWith(groupName))}
placeholder="Select projects..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
)}
</>
);
};

View File

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

View File

@@ -0,0 +1,49 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
export const GitLabDataSourceReviewFields = () => {
const { watch } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitLab;
}
>();
const [config, connection] = watch(["config", "connection"]);
if (config.scope === GitLabDataSourceScope.Project) {
const { projectName, projectId } = config;
return (
<SecretScanningDataSourceConfigReviewSection>
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
<GenericFieldLabel label="Scope" className="capitalize">
{config.scope}
</GenericFieldLabel>
<GenericFieldLabel label="Project">{projectName || projectId}</GenericFieldLabel>
</SecretScanningDataSourceConfigReviewSection>
);
}
// group-scope
const { includeProjects, groupName, groupId } = config;
const shouldScanAll = includeProjects.includes("*");
return (
<SecretScanningDataSourceConfigReviewSection>
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
<GenericFieldLabel label="Scope" className="capitalize">
{config.scope}
</GenericFieldLabel>
<GenericFieldLabel label="Group">{groupName || groupId}</GenericFieldLabel>
<GenericFieldLabel label="Scan Projects">
{shouldScanAll ? "All" : includeProjects.join(", ")}
</GenericFieldLabel>
</SecretScanningDataSourceConfigReviewSection>
);
};

View File

@@ -6,10 +6,12 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { BitbucketDataSourceReviewFields } from "./BitbucketDataSourceReviewFields";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
import { GitLabDataSourceReviewFields } from "./GitLabDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields,
[SecretScanningDataSource.GitLab]: GitLabDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {

View File

@@ -0,0 +1,29 @@
import { z } from "zod";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
export const GitLabDataSourceSchema = z
.object({
type: z.literal(SecretScanningDataSource.GitLab),
config: z.discriminatedUnion("scope", [
z.object({
scope: z.literal(GitLabDataSourceScope.Group),
groupId: z.number(),
groupName: z.string(),
includeProjects: z
.array(z.string().min(1).max(256))
.min(1, "One or more projects required")
.max(100, "Cannot configure more than 100 projects")
.default(["*"])
}),
z.object({
scope: z.literal(GitLabDataSourceScope.Project),
projectId: z.number(),
projectName: z.string()
})
])
})
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));

View File

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

View File

@@ -16,8 +16,8 @@ import {
import {
TGitLabGroup,
TGitLabProject,
useGitlabConnectionListGroups,
useGitlabConnectionListProjects
useGitLabConnectionListGroups,
useGitLabConnectionListProjects
} from "@app/hooks/api/appConnections/gitlab";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
@@ -70,11 +70,11 @@ export const GitLabSyncFields = () => {
const scope = useWatch({ name: "destinationConfig.scope", control });
const shouldMaskSecrets = useWatch({ name: "destinationConfig.shouldMaskSecrets", control });
const { data: groups, isLoading: isGroupsLoading } = useGitlabConnectionListGroups(connectionId, {
const { data: groups, isLoading: isGroupsLoading } = useGitLabConnectionListGroups(connectionId, {
enabled: Boolean(connectionId) && scope === GitLabSyncScope.Group
});
const { data: projects, isLoading: isProjectsLoading } = useGitlabConnectionListProjects(
const { data: projects, isLoading: isProjectsLoading } = useGitLabConnectionListProjects(
connectionId,
{
enabled: Boolean(connectionId)

View File

@@ -94,7 +94,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" },
[AppConnection.Render]: { name: "Render", image: "Render.png" },
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },

View File

@@ -24,6 +24,11 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
name: "Bitbucket",
image: "Bitbucket.png",
size: 45
},
[SecretScanningDataSource.GitLab]: {
name: "GitLab",
image: "GitLab.png",
size: 45
}
};
@@ -32,7 +37,8 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
AppConnection
> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket,
[SecretScanningDataSource.GitLab]: AppConnection.GitLab
};
export const RESOURCE_DESCRIPTION_HELPER: Record<
@@ -58,6 +64,13 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
singularNoun: "repository",
pluralTitle: "Repositories",
singularTitle: "Repository"
},
[SecretScanningDataSource.GitLab]: {
verb: "push",
pluralNoun: "projects",
singularNoun: "project",
pluralTitle: "Projects",
singularTitle: "project"
}
};

View File

@@ -133,7 +133,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Supabase]: AppConnection.Supabase,

View File

@@ -26,7 +26,7 @@ export enum AppConnection {
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
Gitlab = "gitlab",
GitLab = "gitlab",
Cloudflare = "cloudflare",
Bitbucket = "bitbucket",
Zabbix = "zabbix",

View File

@@ -13,7 +13,7 @@ const gitlabConnectionKeys = {
[...gitlabConnectionKeys.all, "groups", connectionId] as const
};
export const useGitlabConnectionListProjects = (
export const useGitLabConnectionListProjects = (
connectionId: string,
options?: Omit<
UseQueryOptions<
@@ -38,7 +38,7 @@ export const useGitlabConnectionListProjects = (
});
};
export const useGitlabConnectionListGroups = (
export const useGitLabConnectionListGroups = (
connectionId: string,
options?: Omit<
UseQueryOptions<

View File

@@ -124,7 +124,7 @@ export type TFlyioConnectionOption = TAppConnectionOptionBase & {
};
export type TGitlabConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Gitlab;
app: AppConnection.GitLab;
oauthClientId?: string;
};
@@ -223,7 +223,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.Heroku]: THerokuConnectionOption;
[AppConnection.Render]: TRenderConnectionOption;
[AppConnection.Flyio]: TFlyioConnectionOption;
[AppConnection.Gitlab]: TGitlabConnectionOption;
[AppConnection.GitLab]: TGitlabConnectionOption;
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
[AppConnection.Zabbix]: TZabbixConnectionOption;

View File

@@ -8,7 +8,7 @@ export enum GitLabConnectionMethod {
OAuth = "oauth"
}
export type TGitLabConnection = TRootAppConnection & { app: AppConnection.Gitlab } & (
export type TGitLabConnection = TRootAppConnection & { app: AppConnection.GitLab } & (
| {
method: GitLabConnectionMethod.AccessToken;
credentials: {

View File

@@ -173,7 +173,7 @@ export type TAppConnectionMap = {
[AppConnection.Heroku]: THerokuConnection;
[AppConnection.Render]: TRenderConnection;
[AppConnection.Flyio]: TFlyioConnection;
[AppConnection.Gitlab]: TGitLabConnection;
[AppConnection.GitLab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Bitbucket]: TBitbucketConnection;
[AppConnection.Zabbix]: TZabbixConnection;

View File

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

View File

@@ -0,0 +1,31 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretScanningDataSource } from "../enums";
import { TSecretScanningDataSourceBase } from "./shared";
export enum GitLabDataSourceScope {
Project = "project",
Group = "group"
}
export type TGitLabDataSource = TSecretScanningDataSourceBase & {
type: SecretScanningDataSource.GitLab;
config:
| {
groupId: number;
groupName?: string;
includeProjects: string[];
scope: GitLabDataSourceScope.Group;
}
| {
projectName?: string;
projectId: number;
scope: GitLabDataSourceScope.Project;
};
};
export type TGitLabDataSourceOption = {
name: string;
type: SecretScanningDataSource.GitLab;
connection: AppConnection.GitLab;
};

View File

@@ -1,3 +1,7 @@
import {
TGitLabDataSource,
TGitLabDataSourceOption
} from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { DiscriminativePick } from "@app/types";
import {
@@ -11,7 +15,10 @@ import {
import { TBitbucketDataSource, TBitbucketDataSourceOption } from "./bitbucket-data-source";
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
export type TSecretScanningDataSource =
| TGitHubDataSource
| TBitbucketDataSource
| TGitLabDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt: string | null;
@@ -24,7 +31,10 @@ export type TListSecretScanningDataSources = {
dataSources: TSecretScanningDataSourceWithDetails[];
};
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitbucketDataSourceOption;
export type TSecretScanningDataSourceOption =
| TGitHubDataSourceOption
| TBitbucketDataSourceOption
| TGitLabDataSourceOption;
export type TListSecretScanningDataSourceOptions = {
dataSourceOptions: TSecretScanningDataSourceOption[];

View File

@@ -29,7 +29,7 @@ export type TGitLabSync = TRootSecretSync & {
shouldHideSecrets?: boolean;
};
connection: {
app: AppConnection.Gitlab;
app: AppConnection.GitLab;
name: string;
id: string;
};

View File

@@ -137,7 +137,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <RenderConnectionForm onSubmit={onSubmit} />;
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} />;
case AppConnection.Gitlab:
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />;
@@ -245,7 +245,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Gitlab:
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;

View File

@@ -38,7 +38,7 @@ type Props = {
const formSchema = z.discriminatedUnion("method", [
genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Gitlab),
app: z.literal(AppConnection.GitLab),
method: z.literal(GitLabConnectionMethod.AccessToken),
credentials: z.object({
accessToken: z.string().min(1, "Access token is required"),
@@ -54,7 +54,7 @@ const formSchema = z.discriminatedUnion("method", [
})
}),
genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Gitlab),
app: z.literal(AppConnection.GitLab),
method: z.literal(GitLabConnectionMethod.OAuth),
credentials: z.object({
code: z.string().min(1, "Code is required"),
@@ -79,7 +79,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
const {
option: { oauthClientId },
isLoading
} = useGetAppConnectionOption(AppConnection.Gitlab);
} = useGetAppConnectionOption(AppConnection.GitLab);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
@@ -88,7 +88,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
? { ...appConnection, credentials: { code: "custom" } }
: (appConnection ??
({
app: AppConnection.Gitlab,
app: AppConnection.GitLab,
method: GitLabConnectionMethod.AccessToken,
credentials: {
accessToken: "",
@@ -207,7 +207,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Gitlab].name
APP_CONNECTION_MAP[AppConnection.GitLab].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig && selectedMethod === GitLabConnectionMethod.OAuth

View File

@@ -65,7 +65,7 @@ type AzureDevOpsFormData = BaseFormData &
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.Gitlab]: GitLabFormData & { app: AppConnection.Gitlab };
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
@@ -81,7 +81,7 @@ type FormDataMap = {
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
[AppConnection.Gitlab]: "gitlabConnectionFormData",
[AppConnection.GitLab]: "gitlabConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
@@ -141,17 +141,17 @@ export const OAuthCallbackPage = () => {
};
const handleGitlab = useCallback(async () => {
const formData = getFormData(AppConnection.Gitlab);
const formData = getFormData(AppConnection.GitLab);
if (formData === null) return null;
clearState(AppConnection.Gitlab);
clearState(AppConnection.GitLab);
const { connectionId, name, description, returnUrl, isUpdate } = formData;
try {
if (isUpdate && connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.Gitlab,
app: AppConnection.GitLab,
connectionId,
credentials: {
code: code as string
@@ -159,7 +159,7 @@ export const OAuthCallbackPage = () => {
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.Gitlab,
app: AppConnection.GitLab,
name,
description,
method: GitLabConnectionMethod.OAuth,
@@ -521,7 +521,7 @@ export const OAuthCallbackPage = () => {
data = await handleGithub();
} else if (appConnection === AppConnection.GitHubRadar) {
data = await handleGithubRadar();
} else if (appConnection === AppConnection.Gitlab) {
} else if (appConnection === AppConnection.GitLab) {
data = await handleGitlab();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();

View File

@@ -5,6 +5,7 @@ import {
import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay";
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
import { GitLabDataSourceConfigDisplay } from "./GitLabDataSourceConfigDisplay";
type Props = {
dataSource: TSecretScanningDataSource;
@@ -16,6 +17,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.Bitbucket:
return <BitbucketDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.GitLab:
return <GitLabDataSourceConfigDisplay dataSource={dataSource} />;
default:
throw new Error(
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`

View File

@@ -0,0 +1,41 @@
import { GenericFieldLabel } from "@app/components/v2";
import {
GitLabDataSourceScope,
TGitLabDataSource
} from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
type Props = {
dataSource: TGitLabDataSource;
};
export const GitLabDataSourceConfigDisplay = ({ dataSource }: Props) => {
const { config } = dataSource;
if (config.scope === GitLabDataSourceScope.Project) {
const { projectName, projectId } = config;
return (
<>
<GenericFieldLabel label="Scope" className="capitalize">
{config.scope}
</GenericFieldLabel>
<GenericFieldLabel label="Project">{projectName || projectId}</GenericFieldLabel>
</>
);
}
// group-scope
const { includeProjects, groupId, groupName } = config;
return (
<>
<GenericFieldLabel label="Scope" className="capitalize">
{config.scope}
</GenericFieldLabel>
<GenericFieldLabel label="Group">{groupName || groupId}</GenericFieldLabel>
<GenericFieldLabel label="Scan Projects">
{includeProjects.includes("*") ? "All" : includeProjects.join(", ")}
</GenericFieldLabel>
</>
);
};

View File

@@ -35,12 +35,14 @@ import {
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { useToggle } from "@app/hooks";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
SecretScanningScanStatus,
TSecretScanningDataSource,
TSecretScanningResourceWithDetails,
useTriggerSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
type Props = {
resource: TSecretScanningResourceWithDetails;
@@ -51,12 +53,30 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
resource;
const {
config: { includeRepos }
} = dataSource;
let isActive: boolean;
switch (dataSource.type) {
case SecretScanningDataSource.Bitbucket:
case SecretScanningDataSource.GitHub:
isActive =
dataSource.config.includeRepos.includes("*") ||
dataSource.config.includeRepos.includes(name);
break;
case SecretScanningDataSource.GitLab: {
if (dataSource.config.scope === GitLabDataSourceScope.Project) {
isActive = true; // always active
} else {
isActive =
dataSource.config.includeProjects.includes("*") ||
dataSource.config.includeProjects.includes(name);
}
break;
}
default:
throw new Error("Unhandled Data Source Type: Active Status");
}
// scott: will need to be differentiated by type once other data sources are available
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
const triggerDataSourceScan = useTriggerSecretScanningDataSource();

View File

@@ -38,10 +38,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
SecretScanningDataSource,
SecretScanningScanStatus,
TSecretScanningDataSource,
useListSecretScanningResources
} from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { SecretScanningResourceRow } from "./SecretScanningResourceRow";
@@ -104,12 +106,29 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
resources
.filter((resource) => {
const { name } = resource;
const {
config: { includeRepos }
} = dataSource;
// scott: will need to be differentiated by type once other data sources are available
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
let isActive: boolean;
switch (dataSource.type) {
case SecretScanningDataSource.Bitbucket:
case SecretScanningDataSource.GitHub:
isActive =
dataSource.config.includeRepos.includes("*") ||
dataSource.config.includeRepos.includes(name);
break;
case SecretScanningDataSource.GitLab: {
if (dataSource.config.scope === GitLabDataSourceScope.Project) {
isActive = true; // always active
} else {
isActive =
dataSource.config.includeProjects.includes("*") ||
dataSource.config.includeProjects.includes(name);
}
break;
}
default:
throw new Error("Unhandled Data Source Type: Active Filter");
}
if (filters.status.length === 1) {
if (filters.status.includes(ResourceStatus.Active) && !isActive) {