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