feature: secret scanning architecture and github data source (wip)

This commit is contained in:
Scott Wilson
2025-05-28 22:21:03 -07:00
parent 931ee1e8da
commit e6c97510ca
231 changed files with 11443 additions and 1048 deletions
@@ -18,6 +18,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { GitHubRadarConnectionForm } from "./GitHubRadarConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
@@ -70,6 +71,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
@@ -144,6 +147,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm appConnection={appConnection} />;
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
@@ -178,7 +183,6 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <TeamCityConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OCI:
return <OCIConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,157 @@
import crypto from "crypto";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubRadarConnectionMethod,
TGitHubRadarConnection,
useGetAppConnectionOption
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TGitHubRadarConnection;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHubRadar),
method: z.nativeEnum(GitHubRadarConnectionMethod)
});
type FormData = z.infer<typeof formSchema>;
export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
const {
option: { appClientSlug },
isLoading
} = useGetAppConnectionOption(AppConnection.GitHubRadar);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.GitHubRadar,
method: GitHubRadarConnectionMethod.App
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case GitHubRadarConnectionMethod.App:
window.location.assign(
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}`
);
break;
default:
throw new Error(
`Unhandled GitHub Radar Connection method: ${(formData as FormData).method}`
);
}
};
let isMissingConfig: boolean;
switch (selectedMethod) {
case GitHubRadarConnectionMethod.App:
isMissingConfig = !appClientSlug;
break;
default:
throw new Error(`Unhandled GitHub Radar Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.GitHubRadar].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See Docs to configure GitHub Radar ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(GitHubRadarConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -15,11 +15,19 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
return (
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div className="relative">
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
{appDetails.icon && (
<FontAwesomeIcon
icon={appDetails.icon}
className="absolute bottom-1 right-1 text-primary-700"
/>
)}
</div>
<div>
<div className="flex items-center text-mineshaft-300">
{appDetails.name}
@@ -1,4 +1,4 @@
import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Spinner, Tooltip } from "@app/components/v2";
@@ -23,69 +23,81 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
}
return (
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => {
const { image, name, size = 50 } = APP_CONNECTION_MAP[option.app];
<>
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => {
const { image, name, size = 50, icon } = APP_CONNECTION_MAP[option.app];
return (
<button
type="button"
key={option.app}
onClick={() => onSelect(option.app)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
<p>
{`If you don't see the third-party
app you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
Coming Soon
</div>
return (
<button
type="button"
key={option.app}
onClick={() => onSelect(option.app)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
{icon && (
<FontAwesomeIcon
className="-right- absolute -bottom-1.5 -right-1.5 text-primary-700"
size="xl"
icon={icon}
/>
)}
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
</div>
<div className="mt-2 flex items-center gap-1">
<div className="max-w-xs text-center text-sm font-medium text-mineshaft-400 duration-200">
Don&#39;t see the connection you&#39;re looking for?
</div>
</Tooltip>
</div>
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
<p>
{`If you don't see the third-party
app you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<FontAwesomeIcon className="text-mineshaft-400" size="xs" icon={faInfoCircle} />
</Tooltip>
</div>
</>
);
};
@@ -65,6 +65,8 @@ export const AppConnectionRow = ({
const methodDetails = getAppConnectionMethodDetails(method);
const connectionDetails = APP_CONNECTION_MAP[app];
return (
<Tr
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
@@ -72,12 +74,21 @@ export const AppConnectionRow = ({
>
<Td>
<div className="flex items-center gap-2">
<img
alt={`${APP_CONNECTION_MAP[app].name} integration`}
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
className="mr-0.5 w-5"
/>
<span className="hidden lg:inline">{APP_CONNECTION_MAP[app].name}</span>
<div className="relative">
<img
alt={`${connectionDetails.name} integration`}
src={`/images/integrations/${connectionDetails.image}`}
className="mr-0.5 w-5"
/>
{connectionDetails.icon && (
<FontAwesomeIcon
icon={connectionDetails.icon}
size="xs"
className="absolute -bottom-0.5 -right-0.5 text-primary-700"
/>
)}
</div>
<span className="hidden lg:inline">{connectionDetails.name}</span>
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">
@@ -14,6 +14,7 @@ import {
TAzureClientSecretsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
@@ -26,6 +27,9 @@ type BaseFormData = {
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
@@ -40,6 +44,7 @@ type AzureClientSecretsFormData = BaseFormData &
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
@@ -51,6 +56,7 @@ type FormDataMap = {
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData"
@@ -318,6 +324,54 @@ export const OAuthCallbackPage = () => {
};
}, []);
const handleGithubRadar = useCallback(async () => {
const formData = getFormData(AppConnection.GitHubRadar);
if (formData === null) return null;
clearState(AppConnection.GitHubRadar);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
code: code as string,
installationId: installationId as string
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
credentials: {
code: code as string,
installationId: installationId as string
}
});
}
} catch (e: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} GitHub Radar Connection`,
text: e.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
useEffect(() => {
if (!isReady) {
@@ -334,6 +388,8 @@ export const OAuthCallbackPage = () => {
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
} else if (appConnection === AppConnection.GitHubRadar) {
data = await handleGithubRadar();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();
} else if (appConnection === AppConnection.AzureAppConfiguration) {