feature: gitlab secret scanning
@@ -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
|
||||||
|
});
|
||||||
@@ -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 { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||||
|
|
||||||
import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
|
import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
|
||||||
@@ -10,5 +11,6 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
|
|||||||
(server: FastifyZodProvider) => Promise<void>
|
(server: FastifyZodProvider) => Promise<void>
|
||||||
> = {
|
> = {
|
||||||
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
|
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
|
||||||
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter
|
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter,
|
||||||
|
[SecretScanningDataSource.GitLab]: registerGitLabSecretScanningRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SecretScanningConfigsSchema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
||||||
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
|
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
|
||||||
|
import { GitLabDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||||
import {
|
import {
|
||||||
SecretScanningFindingStatus,
|
SecretScanningFindingStatus,
|
||||||
SecretScanningScanStatus
|
SecretScanningScanStatus
|
||||||
@@ -24,7 +25,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
|
|
||||||
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
|
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
|
||||||
GitHubDataSourceListItemSchema,
|
GitHubDataSourceListItemSchema,
|
||||||
BitbucketDataSourceListItemSchema
|
BitbucketDataSourceListItemSchema,
|
||||||
|
GitLabDataSourceListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {
|
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
TSecretScanningFactoryInitialize,
|
TSecretScanningFactoryInitialize,
|
||||||
TSecretScanningFactoryListRawResources,
|
TSecretScanningFactoryListRawResources,
|
||||||
TSecretScanningFactoryPostInitialization,
|
TSecretScanningFactoryPostInitialization,
|
||||||
TSecretScanningFactoryTeardown
|
TSecretScanningFactoryTeardown,
|
||||||
|
TSecretScanningFactoryValidateConfigUpdate
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
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 {
|
return {
|
||||||
initialize,
|
initialize,
|
||||||
postInitialization,
|
postInitialization,
|
||||||
@@ -309,6 +317,7 @@ export const BitbucketSecretScanningFactory = () => {
|
|||||||
getFullScanPath,
|
getFullScanPath,
|
||||||
getDiffScanResourcePayload,
|
getDiffScanResourcePayload,
|
||||||
getDiffScanFindingsPayload,
|
getDiffScanFindingsPayload,
|
||||||
teardown
|
teardown,
|
||||||
|
validateConfigUpdate
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import {
|
|||||||
TSecretScanningFactoryInitialize,
|
TSecretScanningFactoryInitialize,
|
||||||
TSecretScanningFactoryListRawResources,
|
TSecretScanningFactoryListRawResources,
|
||||||
TSecretScanningFactoryPostInitialization,
|
TSecretScanningFactoryPostInitialization,
|
||||||
TSecretScanningFactoryTeardown
|
TSecretScanningFactoryTeardown,
|
||||||
|
TSecretScanningFactoryValidateConfigUpdate
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -64,7 +65,14 @@ export const GitHubSecretScanningFactory = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const teardown: TSecretScanningFactoryTeardown<TGitHubDataSourceWithConnection> = async () => {
|
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 (
|
const listRawResources: TSecretScanningFactoryListRawResources<TGitHubDataSourceWithConnection> = async (
|
||||||
@@ -238,6 +246,7 @@ export const GitHubSecretScanningFactory = () => {
|
|||||||
getFullScanPath,
|
getFullScanPath,
|
||||||
getDiffScanResourcePayload,
|
getDiffScanResourcePayload,
|
||||||
getDiffScanFindingsPayload,
|
getDiffScanFindingsPayload,
|
||||||
teardown
|
teardown,
|
||||||
|
validateConfigUpdate
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export enum GitLabDataSourceScope {
|
||||||
|
Project = "project",
|
||||||
|
Group = "group"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GitLabWebHookEvent {
|
||||||
|
Push = "Push Hook"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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()
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./gitlab-secret-scanning-constants";
|
||||||
|
export * from "./gitlab-secret-scanning-schemas";
|
||||||
|
export * from "./gitlab-secret-scanning-types";
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum SecretScanningDataSource {
|
export enum SecretScanningDataSource {
|
||||||
GitHub = "github",
|
GitHub = "github",
|
||||||
Bitbucket = "bitbucket"
|
Bitbucket = "bitbucket",
|
||||||
|
GitLab = "gitlab"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretScanningScanStatus {
|
export enum SecretScanningScanStatus {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
|
import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
|
||||||
import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
|
import { 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 { SecretScanningDataSource } from "./secret-scanning-v2-enums";
|
||||||
import {
|
import {
|
||||||
@@ -19,5 +20,6 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory<
|
|||||||
|
|
||||||
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
|
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
|
||||||
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
|
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
|
||||||
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation
|
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation,
|
||||||
|
[SecretScanningDataSource.GitLab]: GitLabSecretScanningFactory as TSecretScanningFactoryImplementation
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { 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 { 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 { 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 { titleCaseToCamelCase } from "@app/lib/fn";
|
||||||
|
|
||||||
import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
|
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> = {
|
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
|
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
|
||||||
|
[SecretScanningDataSource.GitLab]: GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSecretScanningDataSourceOptions = () => {
|
export const listSecretScanningDataSourceOptions = () => {
|
||||||
|
|||||||
@@ -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> = {
|
export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = {
|
||||||
[SecretScanningDataSource.GitHub]: "GitHub",
|
[SecretScanningDataSource.GitHub]: "GitHub",
|
||||||
[SecretScanningDataSource.Bitbucket]: "Bitbucket"
|
[SecretScanningDataSource.Bitbucket]: "Bitbucket",
|
||||||
|
[SecretScanningDataSource.GitLab]: "GitLab"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
|
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
|
||||||
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
|
[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 }> = {
|
export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
|
||||||
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
|
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
|
||||||
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" }
|
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" },
|
||||||
|
[SecretScanningDataSource.GitLab]: { verb: "push", noun: "projects" }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
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 { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -48,6 +49,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||||
};
|
};
|
||||||
@@ -62,7 +64,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
kmsService,
|
kmsService,
|
||||||
auditLogService,
|
auditLogService,
|
||||||
keyStore
|
keyStore,
|
||||||
|
appConnectionDAL
|
||||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||||
const queueDataSourceFullScan = async (
|
const queueDataSourceFullScan = async (
|
||||||
dataSource: TSecretScanningDataSourceWithConnection,
|
dataSource: TSecretScanningDataSourceWithConnection,
|
||||||
@@ -71,7 +74,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
try {
|
try {
|
||||||
const { type } = dataSource;
|
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);
|
const rawResources = await factory.listRawResources(dataSource);
|
||||||
|
|
||||||
@@ -171,7 +177,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
let connection: TAppConnection | null = null;
|
let connection: TAppConnection | null = null;
|
||||||
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
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");
|
const findingsPath = join(tempFolder, "findings.json");
|
||||||
|
|
||||||
@@ -329,7 +338,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
dataSourceId,
|
dataSourceId,
|
||||||
dataSourceType
|
dataSourceType
|
||||||
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "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);
|
const resourcePayload = factory.getDiffScanResourcePayload(payload);
|
||||||
|
|
||||||
@@ -391,7 +403,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
|
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();
|
const tempFolder = await createTempFolder();
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
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 { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
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 { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { bitbucketSecretScanningService } from "./bitbucket/bitbucket-secret-scanning-service";
|
import { bitbucketSecretScanningService } from "./bitbucket/bitbucket-secret-scanning-service";
|
||||||
|
import { gitlabSecretScanningService } from "./gitlab/gitlab-secret-scanning-service";
|
||||||
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
||||||
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
|
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
|
||||||
|
|
||||||
export type TSecretScanningV2ServiceFactoryDep = {
|
export type TSecretScanningV2ServiceFactoryDep = {
|
||||||
secretScanningV2DAL: TSecretScanningV2DALFactory;
|
secretScanningV2DAL: TSecretScanningV2DALFactory;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
secretScanningV2Queue: Pick<
|
secretScanningV2Queue: Pick<
|
||||||
@@ -76,6 +79,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
appConnectionService,
|
appConnectionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
secretScanningV2Queue,
|
secretScanningV2Queue,
|
||||||
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}: TSecretScanningV2ServiceFactoryDep) => {
|
}: TSecretScanningV2ServiceFactoryDep) => {
|
||||||
const $checkListSecretScanningDataSourcesByProjectIdPermissions = async (
|
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 {
|
try {
|
||||||
const createdDataSource = await factory.initialize(
|
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]}`
|
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 {
|
try {
|
||||||
const updatedDataSource = await secretScanningV2DAL.dataSources.updateById(dataSourceId, payload);
|
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]}`
|
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;
|
let connection: TAppConnection | null = null;
|
||||||
if (dataSource.connection) {
|
if (dataSource.connection) {
|
||||||
@@ -903,6 +938,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
findSecretScanningConfigByProjectId,
|
findSecretScanningConfigByProjectId,
|
||||||
upsertSecretScanningConfig,
|
upsertSecretScanningConfig,
|
||||||
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
|
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
|
||||||
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
|
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService),
|
||||||
|
gitlab: gitlabSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,14 +21,25 @@ import {
|
|||||||
TGitHubFinding,
|
TGitHubFinding,
|
||||||
TQueueGitHubResourceDiffScan
|
TQueueGitHubResourceDiffScan
|
||||||
} from "@app/ee/services/secret-scanning-v2/github";
|
} 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 { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
|
||||||
import {
|
import {
|
||||||
SecretScanningDataSource,
|
SecretScanningDataSource,
|
||||||
SecretScanningFindingStatus,
|
SecretScanningFindingStatus,
|
||||||
SecretScanningScanStatus
|
SecretScanningScanStatus
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
} 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 & {
|
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
|
||||||
lastScannedAt?: Date | null;
|
lastScannedAt?: Date | null;
|
||||||
@@ -52,15 +63,25 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
|
|||||||
|
|
||||||
export type TSecretScanningDataSourceWithConnection =
|
export type TSecretScanningDataSourceWithConnection =
|
||||||
| TGitHubDataSourceWithConnection
|
| 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 = {
|
export type TListSecretScanningDataSourcesByProjectId = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -112,7 +133,10 @@ export type TQueueSecretScanningDataSourceFullScan = {
|
|||||||
scanId: string;
|
scanId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan;
|
export type TQueueSecretScanningResourceDiffScan =
|
||||||
|
| TQueueGitHubResourceDiffScan
|
||||||
|
| TQueueBitbucketResourceDiffScan
|
||||||
|
| TQueueGitLabResourceDiffScan;
|
||||||
|
|
||||||
export type TQueueSecretScanningSendNotification = {
|
export type TQueueSecretScanningSendNotification = {
|
||||||
dataSource: TSecretScanningDataSources;
|
dataSource: TSecretScanningDataSources;
|
||||||
@@ -170,6 +194,11 @@ export type TSecretScanningFactoryInitialize<
|
|||||||
callback: (parameters: { credentials?: C; externalId?: string }) => Promise<TSecretScanningDataSourceRaw>
|
callback: (parameters: { credentials?: C; externalId?: string }) => Promise<TSecretScanningDataSourceRaw>
|
||||||
) => Promise<TSecretScanningDataSourceRaw>;
|
) => Promise<TSecretScanningDataSourceRaw>;
|
||||||
|
|
||||||
|
export type TSecretScanningFactoryValidateConfigUpdate<
|
||||||
|
C extends TSecretScanningDataSourceInput["config"],
|
||||||
|
T extends TSecretScanningDataSourceWithConnection
|
||||||
|
> = (params: { config: C; dataSource: T }) => Promise<void>;
|
||||||
|
|
||||||
export type TSecretScanningFactoryPostInitialization<
|
export type TSecretScanningFactoryPostInitialization<
|
||||||
P extends TSecretScanningDataSourceInput,
|
P extends TSecretScanningDataSourceInput,
|
||||||
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
|
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
|
||||||
@@ -181,17 +210,23 @@ export type TSecretScanningFactoryTeardown<
|
|||||||
C extends TSecretScanningDataSourceCredentials = undefined
|
C extends TSecretScanningDataSourceCredentials = undefined
|
||||||
> = (params: { dataSource: T; credentials: C }) => Promise<void>;
|
> = (params: { dataSource: T; credentials: C }) => Promise<void>;
|
||||||
|
|
||||||
|
export type TSecretScanningFactoryParams = {
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
};
|
||||||
|
|
||||||
export type TSecretScanningFactory<
|
export type TSecretScanningFactory<
|
||||||
T extends TSecretScanningDataSourceWithConnection,
|
T extends TSecretScanningDataSourceWithConnection,
|
||||||
P extends TQueueSecretScanningResourceDiffScan["payload"],
|
P extends TQueueSecretScanningResourceDiffScan["payload"],
|
||||||
I extends TSecretScanningDataSourceInput,
|
I extends TSecretScanningDataSourceInput,
|
||||||
C extends TSecretScanningDataSourceCredentials | undefined = undefined
|
C extends TSecretScanningDataSourceCredentials | undefined = undefined
|
||||||
> = () => {
|
> = (params: TSecretScanningFactoryParams) => {
|
||||||
listRawResources: TSecretScanningFactoryListRawResources<T>;
|
listRawResources: TSecretScanningFactoryListRawResources<T>;
|
||||||
getFullScanPath: TSecretScanningFactoryGetFullScanPath<T>;
|
getFullScanPath: TSecretScanningFactoryGetFullScanPath<T>;
|
||||||
initialize: TSecretScanningFactoryInitialize<I, T["connection"] | undefined, C>;
|
initialize: TSecretScanningFactoryInitialize<I, T["connection"] | undefined, C>;
|
||||||
postInitialization: TSecretScanningFactoryPostInitialization<I, T["connection"] | undefined, C>;
|
postInitialization: TSecretScanningFactoryPostInitialization<I, T["connection"] | undefined, C>;
|
||||||
teardown: TSecretScanningFactoryTeardown<T, C>;
|
teardown: TSecretScanningFactoryTeardown<T, C>;
|
||||||
|
validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<I["config"], T>;
|
||||||
getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<P>;
|
getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<P>;
|
||||||
getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<T, P>;
|
getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<T, P>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
||||||
import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github";
|
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", [
|
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
||||||
GitHubDataSourceSchema,
|
GitHubDataSourceSchema,
|
||||||
BitbucketDataSourceSchema
|
BitbucketDataSourceSchema,
|
||||||
|
GitLabDataSourceSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
|
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
|
||||||
@@ -18,5 +20,10 @@ export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType"
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
title: "Bitbucket"
|
title: "Bitbucket"
|
||||||
})
|
})
|
||||||
|
),
|
||||||
|
GitLabFindingSchema.describe(
|
||||||
|
JSON.stringify({
|
||||||
|
title: "GitLab"
|
||||||
|
})
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -2701,6 +2701,14 @@ export const SecretScanningDataSources = {
|
|||||||
GITHUB: {
|
GITHUB: {
|
||||||
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
|
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: {
|
BITBUCKET: {
|
||||||
workspaceSlug: "The workspace to scan.",
|
workspaceSlug: "The workspace to scan.",
|
||||||
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
|
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
|
||||||
|
|||||||
@@ -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 LdapUrlRegex = new RE2(/^ldaps?:\/\//);
|
||||||
|
|
||||||
export const BasicRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/);
|
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._-]+)+$/);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Probot } from "probot";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types";
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -113,4 +115,36 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
|||||||
return res.send("ok");
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1931,7 +1931,8 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
kmsService,
|
kmsService,
|
||||||
keyStore
|
keyStore,
|
||||||
|
appConnectionDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||||
@@ -1940,7 +1941,8 @@ export const registerRoutes = async (
|
|||||||
licenseService,
|
licenseService,
|
||||||
secretScanningV2DAL,
|
secretScanningV2DAL,
|
||||||
secretScanningV2Queue,
|
secretScanningV2Queue,
|
||||||
kmsService
|
kmsService,
|
||||||
|
appConnectionDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
|
|||||||
@@ -222,6 +222,37 @@ export const validateGitLabConnectionCredentials = async (config: TGitLabConnect
|
|||||||
return inputCredentials;
|
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 ({
|
export const listGitLabProjects = async ({
|
||||||
appConnection,
|
appConnection,
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/data-source-name/{dataSourceName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Resources"
|
||||||
|
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/resources"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Scans"
|
||||||
|
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/scans"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Scan Resource"
|
||||||
|
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/resources/{resourceId}/scan"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Scan"
|
||||||
|
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/scan"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
|
||||||
|
---
|
||||||
@@ -220,7 +220,8 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/secret-scanning/overview",
|
"documentation/platform/secret-scanning/overview",
|
||||||
"documentation/platform/secret-scanning/bitbucket",
|
"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",
|
||||||
"api-reference/endpoints/secret-scanning/data-sources/github/scan-resource"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **GitLab** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Configure which workspace and repositories you would like to scan. Then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
6. Your **GitLab Data Source** is now available and will begin a full scan if **Auto-Scan** is enabled.
|
||||||
|

|
||||||
|
|
||||||
|
7. You can view repositories and scan results by clicking on your data source.
|
||||||
|

|
||||||
|
|
||||||
|
8. In addition, you can review any findings from the **Findings Page**.
|
||||||
|

|
||||||
|
</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>
|
||||||
|
Before Width: | Height: | Size: 531 KiB After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 464 KiB After Width: | Height: | Size: 464 KiB |
@@ -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`.
|
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/gitlab/oauth/callback`.
|
||||||
|
|
||||||

|
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:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
<Tab title="Secret Scanning">
|
||||||
|
For Secret Scanning, your application will require the `api` and `read_repository` scopes:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance.
|
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
|
|||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Configure Token">
|
<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:
|
Fill in the token details:
|
||||||
- **Token name**: A descriptive name for the token (e.g., "connection-token")
|
- **Token name**: A descriptive name for the token (e.g., "connection-token")
|
||||||
- **Expiration date**: Set an appropriate expiration date
|
- **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:
|
||||||
|
|
||||||

|
<Tabs>
|
||||||
|
<Tab title="Secret Sync">
|
||||||
|
For Secret Syncs, your token will require the `api` scope:
|
||||||
|
|
||||||
|

|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab title="Secret Scanning">
|
||||||
|
For Secret Scanning, your token will require the `api` and `read_repository` scopes:
|
||||||
|
|
||||||
|

|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
@@ -134,17 +154,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
|||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Configure Token">
|
<Step title="Configure Token">
|
||||||
<Tabs>
|
Fill in the token details:
|
||||||
<Tab title="Secret Sync">
|
- **Token name**: A descriptive name for the token
|
||||||
For Secret Syncs, your token will require the ability to access the API and be at least an **Owner**:
|
- **Expiration date**: Set an appropriate expiration date
|
||||||
Fill in the token details:
|
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
|
||||||
- **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
|
|
||||||
|
|
||||||

|
<Tabs>
|
||||||
</Tab>
|
<Tab title="Secret Sync">
|
||||||
|
For Secret Syncs, your token will require the `api` scope and at least the **Owner** role:
|
||||||
|
|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
<Tab title="Secret Scanning">
|
||||||
|
For Secret Scanning, your token will require the `api` and `read_repository` scopes and the **Maintainer** role:
|
||||||
|
|
||||||
|

|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
|
|||||||
@@ -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't see the project you'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't see the group you'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't see the project you'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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,10 +7,12 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
|||||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||||
import { BitbucketDataSourceConfigFields } from "./BitbucketDataSourceConfigFields";
|
import { BitbucketDataSourceConfigFields } from "./BitbucketDataSourceConfigFields";
|
||||||
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
|
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
|
||||||
|
import { GitLabDataSourceConfigFields } from "./GitLabDataSourceConfigFields";
|
||||||
|
|
||||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
|
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
|
||||||
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields
|
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields,
|
||||||
|
[SecretScanningDataSource.GitLab]: GitLabDataSourceConfigFields
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretScanningDataSourceConfigFields = () => {
|
export const SecretScanningDataSourceConfigFields = () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,10 +6,12 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
|||||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||||
import { BitbucketDataSourceReviewFields } from "./BitbucketDataSourceReviewFields";
|
import { BitbucketDataSourceReviewFields } from "./BitbucketDataSourceReviewFields";
|
||||||
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
|
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
|
||||||
|
import { GitLabDataSourceReviewFields } from "./GitLabDataSourceReviewFields";
|
||||||
|
|
||||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
|
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
|
||||||
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields
|
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields,
|
||||||
|
[SecretScanningDataSource.GitLab]: GitLabDataSourceReviewFields
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretScanningDataSourceReviewFields = () => {
|
export const SecretScanningDataSourceReviewFields = () => {
|
||||||
|
|||||||
@@ -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 }));
|
||||||
@@ -2,10 +2,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BitbucketDataSourceSchema } from "./bitbucket-data-source-schema";
|
import { BitbucketDataSourceSchema } from "./bitbucket-data-source-schema";
|
||||||
import { GitHubDataSourceSchema } from "./github-data-source-schema";
|
import { GitHubDataSourceSchema } from "./github-data-source-schema";
|
||||||
|
import { GitLabDataSourceSchema } from "./gitlab-data-source-schema";
|
||||||
|
|
||||||
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
||||||
GitHubDataSourceSchema,
|
GitHubDataSourceSchema,
|
||||||
BitbucketDataSourceSchema
|
BitbucketDataSourceSchema,
|
||||||
|
GitLabDataSourceSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
|
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
TGitLabGroup,
|
TGitLabGroup,
|
||||||
TGitLabProject,
|
TGitLabProject,
|
||||||
useGitlabConnectionListGroups,
|
useGitLabConnectionListGroups,
|
||||||
useGitlabConnectionListProjects
|
useGitLabConnectionListProjects
|
||||||
} from "@app/hooks/api/appConnections/gitlab";
|
} from "@app/hooks/api/appConnections/gitlab";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
|
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 scope = useWatch({ name: "destinationConfig.scope", control });
|
||||||
const shouldMaskSecrets = useWatch({ name: "destinationConfig.shouldMaskSecrets", 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
|
enabled: Boolean(connectionId) && scope === GitLabSyncScope.Group
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: projects, isLoading: isProjectsLoading } = useGitlabConnectionListProjects(
|
const { data: projects, isLoading: isProjectsLoading } = useGitLabConnectionListProjects(
|
||||||
connectionId,
|
connectionId,
|
||||||
{
|
{
|
||||||
enabled: Boolean(connectionId)
|
enabled: Boolean(connectionId)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const APP_CONNECTION_MAP: Record<
|
|||||||
[AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" },
|
[AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" },
|
||||||
[AppConnection.Render]: { name: "Render", image: "Render.png" },
|
[AppConnection.Render]: { name: "Render", image: "Render.png" },
|
||||||
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
[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.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
||||||
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
|
|||||||
name: "Bitbucket",
|
name: "Bitbucket",
|
||||||
image: "Bitbucket.png",
|
image: "Bitbucket.png",
|
||||||
size: 45
|
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
|
AppConnection
|
||||||
> = {
|
> = {
|
||||||
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
|
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
|
||||||
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
|
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket,
|
||||||
|
[SecretScanningDataSource.GitLab]: AppConnection.GitLab
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RESOURCE_DESCRIPTION_HELPER: Record<
|
export const RESOURCE_DESCRIPTION_HELPER: Record<
|
||||||
@@ -58,6 +64,13 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
|
|||||||
singularNoun: "repository",
|
singularNoun: "repository",
|
||||||
pluralTitle: "Repositories",
|
pluralTitle: "Repositories",
|
||||||
singularTitle: "Repository"
|
singularTitle: "Repository"
|
||||||
|
},
|
||||||
|
[SecretScanningDataSource.GitLab]: {
|
||||||
|
verb: "push",
|
||||||
|
pluralNoun: "projects",
|
||||||
|
singularNoun: "project",
|
||||||
|
pluralTitle: "Projects",
|
||||||
|
singularTitle: "project"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.Heroku]: AppConnection.Heroku,
|
[SecretSync.Heroku]: AppConnection.Heroku,
|
||||||
[SecretSync.Render]: AppConnection.Render,
|
[SecretSync.Render]: AppConnection.Render,
|
||||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||||
[SecretSync.GitLab]: AppConnection.Gitlab,
|
[SecretSync.GitLab]: AppConnection.GitLab,
|
||||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
|
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
|
||||||
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
|
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
|
||||||
[SecretSync.Supabase]: AppConnection.Supabase,
|
[SecretSync.Supabase]: AppConnection.Supabase,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export enum AppConnection {
|
|||||||
Heroku = "heroku",
|
Heroku = "heroku",
|
||||||
Render = "render",
|
Render = "render",
|
||||||
Flyio = "flyio",
|
Flyio = "flyio",
|
||||||
Gitlab = "gitlab",
|
GitLab = "gitlab",
|
||||||
Cloudflare = "cloudflare",
|
Cloudflare = "cloudflare",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
Zabbix = "zabbix",
|
Zabbix = "zabbix",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const gitlabConnectionKeys = {
|
|||||||
[...gitlabConnectionKeys.all, "groups", connectionId] as const
|
[...gitlabConnectionKeys.all, "groups", connectionId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGitlabConnectionListProjects = (
|
export const useGitLabConnectionListProjects = (
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
@@ -38,7 +38,7 @@ export const useGitlabConnectionListProjects = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGitlabConnectionListGroups = (
|
export const useGitLabConnectionListGroups = (
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export type TFlyioConnectionOption = TAppConnectionOptionBase & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TGitlabConnectionOption = TAppConnectionOptionBase & {
|
export type TGitlabConnectionOption = TAppConnectionOptionBase & {
|
||||||
app: AppConnection.Gitlab;
|
app: AppConnection.GitLab;
|
||||||
oauthClientId?: string;
|
oauthClientId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ export type TAppConnectionOptionMap = {
|
|||||||
[AppConnection.Heroku]: THerokuConnectionOption;
|
[AppConnection.Heroku]: THerokuConnectionOption;
|
||||||
[AppConnection.Render]: TRenderConnectionOption;
|
[AppConnection.Render]: TRenderConnectionOption;
|
||||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||||
[AppConnection.Gitlab]: TGitlabConnectionOption;
|
[AppConnection.GitLab]: TGitlabConnectionOption;
|
||||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export enum GitLabConnectionMethod {
|
|||||||
OAuth = "oauth"
|
OAuth = "oauth"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TGitLabConnection = TRootAppConnection & { app: AppConnection.Gitlab } & (
|
export type TGitLabConnection = TRootAppConnection & { app: AppConnection.GitLab } & (
|
||||||
| {
|
| {
|
||||||
method: GitLabConnectionMethod.AccessToken;
|
method: GitLabConnectionMethod.AccessToken;
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export type TAppConnectionMap = {
|
|||||||
[AppConnection.Heroku]: THerokuConnection;
|
[AppConnection.Heroku]: THerokuConnection;
|
||||||
[AppConnection.Render]: TRenderConnection;
|
[AppConnection.Render]: TRenderConnection;
|
||||||
[AppConnection.Flyio]: TFlyioConnection;
|
[AppConnection.Flyio]: TFlyioConnection;
|
||||||
[AppConnection.Gitlab]: TGitLabConnection;
|
[AppConnection.GitLab]: TGitLabConnection;
|
||||||
[AppConnection.Cloudflare]: TCloudflareConnection;
|
[AppConnection.Cloudflare]: TCloudflareConnection;
|
||||||
[AppConnection.Bitbucket]: TBitbucketConnection;
|
[AppConnection.Bitbucket]: TBitbucketConnection;
|
||||||
[AppConnection.Zabbix]: TZabbixConnection;
|
[AppConnection.Zabbix]: TZabbixConnection;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum SecretScanningDataSource {
|
export enum SecretScanningDataSource {
|
||||||
GitHub = "github",
|
GitHub = "github",
|
||||||
Bitbucket = "bitbucket"
|
Bitbucket = "bitbucket",
|
||||||
|
GitLab = "gitlab"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretScanningScanStatus {
|
export enum SecretScanningScanStatus {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
TGitLabDataSource,
|
||||||
|
TGitLabDataSourceOption
|
||||||
|
} from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
|
||||||
import { DiscriminativePick } from "@app/types";
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +15,10 @@ import {
|
|||||||
import { TBitbucketDataSource, TBitbucketDataSourceOption } from "./bitbucket-data-source";
|
import { TBitbucketDataSource, TBitbucketDataSourceOption } from "./bitbucket-data-source";
|
||||||
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
|
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
|
||||||
|
|
||||||
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
|
export type TSecretScanningDataSource =
|
||||||
|
| TGitHubDataSource
|
||||||
|
| TBitbucketDataSource
|
||||||
|
| TGitLabDataSource;
|
||||||
|
|
||||||
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
|
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
|
||||||
lastScannedAt: string | null;
|
lastScannedAt: string | null;
|
||||||
@@ -24,7 +31,10 @@ export type TListSecretScanningDataSources = {
|
|||||||
dataSources: TSecretScanningDataSourceWithDetails[];
|
dataSources: TSecretScanningDataSourceWithDetails[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitbucketDataSourceOption;
|
export type TSecretScanningDataSourceOption =
|
||||||
|
| TGitHubDataSourceOption
|
||||||
|
| TBitbucketDataSourceOption
|
||||||
|
| TGitLabDataSourceOption;
|
||||||
|
|
||||||
export type TListSecretScanningDataSourceOptions = {
|
export type TListSecretScanningDataSourceOptions = {
|
||||||
dataSourceOptions: TSecretScanningDataSourceOption[];
|
dataSourceOptions: TSecretScanningDataSourceOption[];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export type TGitLabSync = TRootSecretSync & {
|
|||||||
shouldHideSecrets?: boolean;
|
shouldHideSecrets?: boolean;
|
||||||
};
|
};
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection.Gitlab;
|
app: AppConnection.GitLab;
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
|||||||
return <RenderConnectionForm onSubmit={onSubmit} />;
|
return <RenderConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Flyio:
|
case AppConnection.Flyio:
|
||||||
return <FlyioConnectionForm onSubmit={onSubmit} />;
|
return <FlyioConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Gitlab:
|
case AppConnection.GitLab:
|
||||||
return <GitLabConnectionForm onSubmit={onSubmit} />;
|
return <GitLabConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Cloudflare:
|
case AppConnection.Cloudflare:
|
||||||
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
||||||
@@ -245,7 +245,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Flyio:
|
case AppConnection.Flyio:
|
||||||
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Gitlab:
|
case AppConnection.GitLab:
|
||||||
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Cloudflare:
|
case AppConnection.Cloudflare:
|
||||||
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type Props = {
|
|||||||
|
|
||||||
const formSchema = z.discriminatedUnion("method", [
|
const formSchema = z.discriminatedUnion("method", [
|
||||||
genericAppConnectionFieldsSchema.extend({
|
genericAppConnectionFieldsSchema.extend({
|
||||||
app: z.literal(AppConnection.Gitlab),
|
app: z.literal(AppConnection.GitLab),
|
||||||
method: z.literal(GitLabConnectionMethod.AccessToken),
|
method: z.literal(GitLabConnectionMethod.AccessToken),
|
||||||
credentials: z.object({
|
credentials: z.object({
|
||||||
accessToken: z.string().min(1, "Access token is required"),
|
accessToken: z.string().min(1, "Access token is required"),
|
||||||
@@ -54,7 +54,7 @@ const formSchema = z.discriminatedUnion("method", [
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
genericAppConnectionFieldsSchema.extend({
|
genericAppConnectionFieldsSchema.extend({
|
||||||
app: z.literal(AppConnection.Gitlab),
|
app: z.literal(AppConnection.GitLab),
|
||||||
method: z.literal(GitLabConnectionMethod.OAuth),
|
method: z.literal(GitLabConnectionMethod.OAuth),
|
||||||
credentials: z.object({
|
credentials: z.object({
|
||||||
code: z.string().min(1, "Code is required"),
|
code: z.string().min(1, "Code is required"),
|
||||||
@@ -79,7 +79,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
|
|||||||
const {
|
const {
|
||||||
option: { oauthClientId },
|
option: { oauthClientId },
|
||||||
isLoading
|
isLoading
|
||||||
} = useGetAppConnectionOption(AppConnection.Gitlab);
|
} = useGetAppConnectionOption(AppConnection.GitLab);
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -88,7 +88,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
|
|||||||
? { ...appConnection, credentials: { code: "custom" } }
|
? { ...appConnection, credentials: { code: "custom" } }
|
||||||
: (appConnection ??
|
: (appConnection ??
|
||||||
({
|
({
|
||||||
app: AppConnection.Gitlab,
|
app: AppConnection.GitLab,
|
||||||
method: GitLabConnectionMethod.AccessToken,
|
method: GitLabConnectionMethod.AccessToken,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessToken: "",
|
accessToken: "",
|
||||||
@@ -207,7 +207,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
|
|||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
tooltipText={`The method you would like to use to connect with ${
|
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.`}
|
}. This field cannot be changed after creation.`}
|
||||||
errorText={
|
errorText={
|
||||||
!isLoading && isMissingConfig && selectedMethod === GitLabConnectionMethod.OAuth
|
!isLoading && isMissingConfig && selectedMethod === GitLabConnectionMethod.OAuth
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ type AzureDevOpsFormData = BaseFormData &
|
|||||||
type FormDataMap = {
|
type FormDataMap = {
|
||||||
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
||||||
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
|
[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.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
|
||||||
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
|
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
|
||||||
app: AppConnection.AzureAppConfiguration;
|
app: AppConnection.AzureAppConfiguration;
|
||||||
@@ -81,7 +81,7 @@ type FormDataMap = {
|
|||||||
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
||||||
[AppConnection.GitHub]: "githubConnectionFormData",
|
[AppConnection.GitHub]: "githubConnectionFormData",
|
||||||
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
|
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
|
||||||
[AppConnection.Gitlab]: "gitlabConnectionFormData",
|
[AppConnection.GitLab]: "gitlabConnectionFormData",
|
||||||
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
||||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
||||||
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
|
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
|
||||||
@@ -141,17 +141,17 @@ export const OAuthCallbackPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGitlab = useCallback(async () => {
|
const handleGitlab = useCallback(async () => {
|
||||||
const formData = getFormData(AppConnection.Gitlab);
|
const formData = getFormData(AppConnection.GitLab);
|
||||||
if (formData === null) return null;
|
if (formData === null) return null;
|
||||||
|
|
||||||
clearState(AppConnection.Gitlab);
|
clearState(AppConnection.GitLab);
|
||||||
|
|
||||||
const { connectionId, name, description, returnUrl, isUpdate } = formData;
|
const { connectionId, name, description, returnUrl, isUpdate } = formData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isUpdate && connectionId) {
|
if (isUpdate && connectionId) {
|
||||||
await updateAppConnection.mutateAsync({
|
await updateAppConnection.mutateAsync({
|
||||||
app: AppConnection.Gitlab,
|
app: AppConnection.GitLab,
|
||||||
connectionId,
|
connectionId,
|
||||||
credentials: {
|
credentials: {
|
||||||
code: code as string
|
code: code as string
|
||||||
@@ -159,7 +159,7 @@ export const OAuthCallbackPage = () => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createAppConnection.mutateAsync({
|
await createAppConnection.mutateAsync({
|
||||||
app: AppConnection.Gitlab,
|
app: AppConnection.GitLab,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
method: GitLabConnectionMethod.OAuth,
|
method: GitLabConnectionMethod.OAuth,
|
||||||
@@ -521,7 +521,7 @@ export const OAuthCallbackPage = () => {
|
|||||||
data = await handleGithub();
|
data = await handleGithub();
|
||||||
} else if (appConnection === AppConnection.GitHubRadar) {
|
} else if (appConnection === AppConnection.GitHubRadar) {
|
||||||
data = await handleGithubRadar();
|
data = await handleGithubRadar();
|
||||||
} else if (appConnection === AppConnection.Gitlab) {
|
} else if (appConnection === AppConnection.GitLab) {
|
||||||
data = await handleGitlab();
|
data = await handleGitlab();
|
||||||
} else if (appConnection === AppConnection.AzureKeyVault) {
|
} else if (appConnection === AppConnection.AzureKeyVault) {
|
||||||
data = await handleAzureKeyVault();
|
data = await handleAzureKeyVault();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
|
|
||||||
import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay";
|
import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay";
|
||||||
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
|
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
|
||||||
|
import { GitLabDataSourceConfigDisplay } from "./GitLabDataSourceConfigDisplay";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dataSource: TSecretScanningDataSource;
|
dataSource: TSecretScanningDataSource;
|
||||||
@@ -16,6 +17,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
|
|||||||
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
|
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
|
||||||
case SecretScanningDataSource.Bitbucket:
|
case SecretScanningDataSource.Bitbucket:
|
||||||
return <BitbucketDataSourceConfigDisplay dataSource={dataSource} />;
|
return <BitbucketDataSourceConfigDisplay dataSource={dataSource} />;
|
||||||
|
case SecretScanningDataSource.GitLab:
|
||||||
|
return <GitLabDataSourceConfigDisplay dataSource={dataSource} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`
|
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -35,12 +35,14 @@ import {
|
|||||||
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
|
SecretScanningDataSource,
|
||||||
SecretScanningFindingStatus,
|
SecretScanningFindingStatus,
|
||||||
SecretScanningScanStatus,
|
SecretScanningScanStatus,
|
||||||
TSecretScanningDataSource,
|
TSecretScanningDataSource,
|
||||||
TSecretScanningResourceWithDetails,
|
TSecretScanningResourceWithDetails,
|
||||||
useTriggerSecretScanningDataSource
|
useTriggerSecretScanningDataSource
|
||||||
} from "@app/hooks/api/secretScanningV2";
|
} from "@app/hooks/api/secretScanningV2";
|
||||||
|
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resource: TSecretScanningResourceWithDetails;
|
resource: TSecretScanningResourceWithDetails;
|
||||||
@@ -51,12 +53,30 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
|
|||||||
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
|
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
|
||||||
resource;
|
resource;
|
||||||
|
|
||||||
const {
|
let isActive: boolean;
|
||||||
config: { includeRepos }
|
|
||||||
} = dataSource;
|
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
|
// scott: will need to be differentiated by type once other data sources are available
|
||||||
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
|
|
||||||
|
|
||||||
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
|||||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import {
|
import {
|
||||||
|
SecretScanningDataSource,
|
||||||
SecretScanningScanStatus,
|
SecretScanningScanStatus,
|
||||||
TSecretScanningDataSource,
|
TSecretScanningDataSource,
|
||||||
useListSecretScanningResources
|
useListSecretScanningResources
|
||||||
} from "@app/hooks/api/secretScanningV2";
|
} from "@app/hooks/api/secretScanningV2";
|
||||||
|
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
|
||||||
|
|
||||||
import { SecretScanningResourceRow } from "./SecretScanningResourceRow";
|
import { SecretScanningResourceRow } from "./SecretScanningResourceRow";
|
||||||
|
|
||||||
@@ -104,12 +106,29 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
|||||||
resources
|
resources
|
||||||
.filter((resource) => {
|
.filter((resource) => {
|
||||||
const { name } = resource;
|
const { name } = resource;
|
||||||
const {
|
|
||||||
config: { includeRepos }
|
|
||||||
} = dataSource;
|
|
||||||
|
|
||||||
// scott: will need to be differentiated by type once other data sources are available
|
let isActive: boolean;
|
||||||
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
|
|
||||||
|
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.length === 1) {
|
||||||
if (filters.status.includes(ResourceStatus.Active) && !isActive) {
|
if (filters.status.includes(ResourceStatus.Active) && !isActive) {
|
||||||
|
|||||||