feature: gitlab secret scanning

This commit is contained in:
Scott Wilson
2025-07-28 15:03:23 -07:00
parent 7d1bc86702
commit f2b0e4ae37
68 changed files with 1711 additions and 94 deletions
@@ -0,0 +1,274 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { MultiValue, SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import {
TGitLabGroup,
TGitLabProject,
useGitLabConnectionListGroups,
useGitLabConnectionListProjects
} from "@app/hooks/api/appConnections/gitlab";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
enum ScanMethod {
AllProjects = "all-projects",
SelectProjects = "select-projects"
}
export const GitLabDataSourceConfigFields = () => {
const { control, watch, setValue, formState } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitLab;
}
>();
console.log(formState);
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const scope = watch("config.scope");
const groupName = watch("config.groupName");
const includeProjects = watch("config.includeProjects");
const { data: projects, isPending: isProjectsPending } = useGitLabConnectionListProjects(
connectionId,
{ enabled: Boolean(connectionId) }
);
const { data: groups, isPending: isGroupsPending } = useGitLabConnectionListGroups(connectionId, {
enabled: Boolean(connectionId) && scope === GitLabDataSourceScope.Group
});
const scanMethod =
!includeProjects || includeProjects.includes("*")
? ScanMethod.AllProjects
: ScanMethod.SelectProjects;
useEffect(() => {
if (!includeProjects) {
setValue("config.includeProjects", ["*"]);
}
}, [includeProjects]);
const clearAllFields = () => {
setValue("config.includeProjects", []);
setValue("config.projectName", "");
setValue("config.groupName", "");
// @ts-expect-error rhf doesn't like this but we need to reset
setValue("config.projectId", undefined);
// @ts-expect-error rhf doesn't like this but we need to reset
setValue("config.groupId", undefined);
};
return (
<>
<SecretScanningDataSourceConnectionField isUpdate={isUpdate} onChange={clearAllFields} />
<Controller
name="config.scope"
control={control}
defaultValue={GitLabDataSourceScope.Project}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Scope"
helperText={isUpdate ? "Cannot be updated" : undefined}
tooltipText={
<div className="flex flex-col gap-3">
<p>Specify the GitLab scope scanning should be performed at:</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">Project</span>: Scan an
individual GitLab project.
</p>
</li>
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">Group</span>: Scan one or more
projects belonging to a GitLab group.
</p>
</li>
</ul>
</div>
}
>
<Select
value={value}
onValueChange={(v) => {
onChange(v);
clearAllFields();
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
isDisabled={isUpdate}
dropdownContainerClassName="max-w-none"
>
{Object.values(GitLabDataSourceScope).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
{scope === GitLabDataSourceScope.Project ? (
<Controller
name="config.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
isLoading={isProjectsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects?.find((project) => value === Number.parseInt(project.id, 10))}
onChange={(newValue) => {
const project = newValue as SingleValue<TGitLabProject>;
onChange(project ? Number.parseInt(project.id, 10) : null);
setValue("config.projectName", project?.name ?? "");
}}
options={projects}
placeholder="Select project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
) : (
<>
<Controller
name="config.groupId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Group"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the group you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
isLoading={isGroupsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={groups?.find((group) => value === Number.parseInt(group.id, 10))}
onChange={(newValue) => {
const group = newValue as SingleValue<TGitLabGroup>;
onChange(group ? Number.parseInt(group.id, 10) : null);
setValue("config.groupName", group?.name ?? "");
setValue("config.includeProjects", ["*"]);
}}
options={groups}
placeholder="Select group..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
<FormControl label="Scan Projects">
<Select
value={scanMethod}
onValueChange={(val) => {
setValue("config.includeProjects", val === ScanMethod.AllProjects ? ["*"] : []);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!connectionId}
>
{Object.values(ScanMethod).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
{scanMethod === ScanMethod.SelectProjects && (
<Controller
name="config.includeProjects"
defaultValue={["*"]}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Include Projects"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsPending && Boolean(connectionId)}
isDisabled={!connectionId || !groupName}
isMulti
value={projects?.filter((project) => value.includes(project.name))}
onChange={(newValue) => {
onChange(
newValue
? (newValue as MultiValue<TGitLabProject>).map((p) => p.name)
: null
);
}}
options={projects?.filter((project) => project.name.startsWith(groupName))}
placeholder="Select projects..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
)}
</>
);
};
@@ -7,10 +7,12 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { BitbucketDataSourceConfigFields } from "./BitbucketDataSourceConfigFields";
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
import { GitLabDataSourceConfigFields } from "./GitLabDataSourceConfigFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields,
[SecretScanningDataSource.GitLab]: GitLabDataSourceConfigFields
};
export const SecretScanningDataSourceConfigFields = () => {
@@ -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 { BitbucketDataSourceReviewFields } from "./BitbucketDataSourceReviewFields";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
import { GitLabDataSourceReviewFields } from "./GitLabDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields,
[SecretScanningDataSource.GitLab]: GitLabDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {
@@ -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 { GitHubDataSourceSchema } from "./github-data-source-schema";
import { GitLabDataSourceSchema } from "./gitlab-data-source-schema";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema,
BitbucketDataSourceSchema
BitbucketDataSourceSchema,
GitLabDataSourceSchema
]);
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
@@ -16,8 +16,8 @@ import {
import {
TGitLabGroup,
TGitLabProject,
useGitlabConnectionListGroups,
useGitlabConnectionListProjects
useGitLabConnectionListGroups,
useGitLabConnectionListProjects
} from "@app/hooks/api/appConnections/gitlab";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
@@ -70,11 +70,11 @@ export const GitLabSyncFields = () => {
const scope = useWatch({ name: "destinationConfig.scope", control });
const shouldMaskSecrets = useWatch({ name: "destinationConfig.shouldMaskSecrets", control });
const { data: groups, isLoading: isGroupsLoading } = useGitlabConnectionListGroups(connectionId, {
const { data: groups, isLoading: isGroupsLoading } = useGitLabConnectionListGroups(connectionId, {
enabled: Boolean(connectionId) && scope === GitLabSyncScope.Group
});
const { data: projects, isLoading: isProjectsLoading } = useGitlabConnectionListProjects(
const { data: projects, isLoading: isProjectsLoading } = useGitLabConnectionListProjects(
connectionId,
{
enabled: Boolean(connectionId)
+1 -1
View File
@@ -94,7 +94,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" },
[AppConnection.Render]: { name: "Render", image: "Render.png" },
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
+14 -1
View File
@@ -24,6 +24,11 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
name: "Bitbucket",
image: "Bitbucket.png",
size: 45
},
[SecretScanningDataSource.GitLab]: {
name: "GitLab",
image: "GitLab.png",
size: 45
}
};
@@ -32,7 +37,8 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
AppConnection
> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket,
[SecretScanningDataSource.GitLab]: AppConnection.GitLab
};
export const RESOURCE_DESCRIPTION_HELPER: Record<
@@ -58,6 +64,13 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
singularNoun: "repository",
pluralTitle: "Repositories",
singularTitle: "Repository"
},
[SecretScanningDataSource.GitLab]: {
verb: "push",
pluralNoun: "projects",
singularNoun: "project",
pluralTitle: "Projects",
singularTitle: "project"
}
};
+1 -1
View File
@@ -133,7 +133,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Supabase]: AppConnection.Supabase,
@@ -26,7 +26,7 @@ export enum AppConnection {
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
Gitlab = "gitlab",
GitLab = "gitlab",
Cloudflare = "cloudflare",
Bitbucket = "bitbucket",
Zabbix = "zabbix",
@@ -13,7 +13,7 @@ const gitlabConnectionKeys = {
[...gitlabConnectionKeys.all, "groups", connectionId] as const
};
export const useGitlabConnectionListProjects = (
export const useGitLabConnectionListProjects = (
connectionId: string,
options?: Omit<
UseQueryOptions<
@@ -38,7 +38,7 @@ export const useGitlabConnectionListProjects = (
});
};
export const useGitlabConnectionListGroups = (
export const useGitLabConnectionListGroups = (
connectionId: string,
options?: Omit<
UseQueryOptions<
@@ -124,7 +124,7 @@ export type TFlyioConnectionOption = TAppConnectionOptionBase & {
};
export type TGitlabConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Gitlab;
app: AppConnection.GitLab;
oauthClientId?: string;
};
@@ -223,7 +223,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.Heroku]: THerokuConnectionOption;
[AppConnection.Render]: TRenderConnectionOption;
[AppConnection.Flyio]: TFlyioConnectionOption;
[AppConnection.Gitlab]: TGitlabConnectionOption;
[AppConnection.GitLab]: TGitlabConnectionOption;
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
[AppConnection.Zabbix]: TZabbixConnectionOption;
@@ -8,7 +8,7 @@ export enum GitLabConnectionMethod {
OAuth = "oauth"
}
export type TGitLabConnection = TRootAppConnection & { app: AppConnection.Gitlab } & (
export type TGitLabConnection = TRootAppConnection & { app: AppConnection.GitLab } & (
| {
method: GitLabConnectionMethod.AccessToken;
credentials: {
@@ -173,7 +173,7 @@ export type TAppConnectionMap = {
[AppConnection.Heroku]: THerokuConnection;
[AppConnection.Render]: TRenderConnection;
[AppConnection.Flyio]: TFlyioConnection;
[AppConnection.Gitlab]: TGitLabConnection;
[AppConnection.GitLab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Bitbucket]: TBitbucketConnection;
[AppConnection.Zabbix]: TZabbixConnection;
@@ -1,6 +1,7 @@
export enum SecretScanningDataSource {
GitHub = "github",
Bitbucket = "bitbucket"
Bitbucket = "bitbucket",
GitLab = "gitlab"
}
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 {
@@ -11,7 +15,10 @@ import {
import { TBitbucketDataSource, TBitbucketDataSourceOption } from "./bitbucket-data-source";
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
export type TSecretScanningDataSource =
| TGitHubDataSource
| TBitbucketDataSource
| TGitLabDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt: string | null;
@@ -24,7 +31,10 @@ export type TListSecretScanningDataSources = {
dataSources: TSecretScanningDataSourceWithDetails[];
};
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitbucketDataSourceOption;
export type TSecretScanningDataSourceOption =
| TGitHubDataSourceOption
| TBitbucketDataSourceOption
| TGitLabDataSourceOption;
export type TListSecretScanningDataSourceOptions = {
dataSourceOptions: TSecretScanningDataSourceOption[];
@@ -29,7 +29,7 @@ export type TGitLabSync = TRootSecretSync & {
shouldHideSecrets?: boolean;
};
connection: {
app: AppConnection.Gitlab;
app: AppConnection.GitLab;
name: string;
id: string;
};
@@ -137,7 +137,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <RenderConnectionForm onSubmit={onSubmit} />;
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} />;
case AppConnection.Gitlab:
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />;
@@ -245,7 +245,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Gitlab:
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
@@ -38,7 +38,7 @@ type Props = {
const formSchema = z.discriminatedUnion("method", [
genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Gitlab),
app: z.literal(AppConnection.GitLab),
method: z.literal(GitLabConnectionMethod.AccessToken),
credentials: z.object({
accessToken: z.string().min(1, "Access token is required"),
@@ -54,7 +54,7 @@ const formSchema = z.discriminatedUnion("method", [
})
}),
genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Gitlab),
app: z.literal(AppConnection.GitLab),
method: z.literal(GitLabConnectionMethod.OAuth),
credentials: z.object({
code: z.string().min(1, "Code is required"),
@@ -79,7 +79,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
const {
option: { oauthClientId },
isLoading
} = useGetAppConnectionOption(AppConnection.Gitlab);
} = useGetAppConnectionOption(AppConnection.GitLab);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
@@ -88,7 +88,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
? { ...appConnection, credentials: { code: "custom" } }
: (appConnection ??
({
app: AppConnection.Gitlab,
app: AppConnection.GitLab,
method: GitLabConnectionMethod.AccessToken,
credentials: {
accessToken: "",
@@ -207,7 +207,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Gitlab].name
APP_CONNECTION_MAP[AppConnection.GitLab].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig && selectedMethod === GitLabConnectionMethod.OAuth
@@ -65,7 +65,7 @@ type AzureDevOpsFormData = BaseFormData &
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.Gitlab]: GitLabFormData & { app: AppConnection.Gitlab };
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
@@ -81,7 +81,7 @@ type FormDataMap = {
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
[AppConnection.Gitlab]: "gitlabConnectionFormData",
[AppConnection.GitLab]: "gitlabConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
@@ -141,17 +141,17 @@ export const OAuthCallbackPage = () => {
};
const handleGitlab = useCallback(async () => {
const formData = getFormData(AppConnection.Gitlab);
const formData = getFormData(AppConnection.GitLab);
if (formData === null) return null;
clearState(AppConnection.Gitlab);
clearState(AppConnection.GitLab);
const { connectionId, name, description, returnUrl, isUpdate } = formData;
try {
if (isUpdate && connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.Gitlab,
app: AppConnection.GitLab,
connectionId,
credentials: {
code: code as string
@@ -159,7 +159,7 @@ export const OAuthCallbackPage = () => {
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.Gitlab,
app: AppConnection.GitLab,
name,
description,
method: GitLabConnectionMethod.OAuth,
@@ -521,7 +521,7 @@ export const OAuthCallbackPage = () => {
data = await handleGithub();
} else if (appConnection === AppConnection.GitHubRadar) {
data = await handleGithubRadar();
} else if (appConnection === AppConnection.Gitlab) {
} else if (appConnection === AppConnection.GitLab) {
data = await handleGitlab();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();
@@ -5,6 +5,7 @@ import {
import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay";
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
import { GitLabDataSourceConfigDisplay } from "./GitLabDataSourceConfigDisplay";
type Props = {
dataSource: TSecretScanningDataSource;
@@ -16,6 +17,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.Bitbucket:
return <BitbucketDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.GitLab:
return <GitLabDataSourceConfigDisplay dataSource={dataSource} />;
default:
throw new Error(
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`
@@ -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 { useToggle } from "@app/hooks";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
SecretScanningScanStatus,
TSecretScanningDataSource,
TSecretScanningResourceWithDetails,
useTriggerSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
type Props = {
resource: TSecretScanningResourceWithDetails;
@@ -51,12 +53,30 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
resource;
const {
config: { includeRepos }
} = dataSource;
let isActive: boolean;
switch (dataSource.type) {
case SecretScanningDataSource.Bitbucket:
case SecretScanningDataSource.GitHub:
isActive =
dataSource.config.includeRepos.includes("*") ||
dataSource.config.includeRepos.includes(name);
break;
case SecretScanningDataSource.GitLab: {
if (dataSource.config.scope === GitLabDataSourceScope.Project) {
isActive = true; // always active
} else {
isActive =
dataSource.config.includeProjects.includes("*") ||
dataSource.config.includeProjects.includes(name);
}
break;
}
default:
throw new Error("Unhandled Data Source Type: Active Status");
}
// scott: will need to be differentiated by type once other data sources are available
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
@@ -38,10 +38,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
SecretScanningDataSource,
SecretScanningScanStatus,
TSecretScanningDataSource,
useListSecretScanningResources
} from "@app/hooks/api/secretScanningV2";
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
import { SecretScanningResourceRow } from "./SecretScanningResourceRow";
@@ -104,12 +106,29 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
resources
.filter((resource) => {
const { name } = resource;
const {
config: { includeRepos }
} = dataSource;
// scott: will need to be differentiated by type once other data sources are available
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
let isActive: boolean;
switch (dataSource.type) {
case SecretScanningDataSource.Bitbucket:
case SecretScanningDataSource.GitHub:
isActive =
dataSource.config.includeRepos.includes("*") ||
dataSource.config.includeRepos.includes(name);
break;
case SecretScanningDataSource.GitLab: {
if (dataSource.config.scope === GitLabDataSourceScope.Project) {
isActive = true; // always active
} else {
isActive =
dataSource.config.includeProjects.includes("*") ||
dataSource.config.includeProjects.includes(name);
}
break;
}
default:
throw new Error("Unhandled Data Source Type: Active Filter");
}
if (filters.status.length === 1) {
if (filters.status.includes(ResourceStatus.Active) && !isActive) {