Merge pull request #3677 from Infisical/secret-scanning-v2-pt-1

feature(secret-scanning-v2): secret scanning architecture and github data source
This commit is contained in:
Scott Wilson
2025-06-03 16:34:29 -07:00
committed by GitHub
344 changed files with 13130 additions and 1118 deletions
@@ -80,7 +80,7 @@ export const OrgGroupsSection = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
title={`Are you sure you want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
@@ -127,7 +127,7 @@ export const IdentitySection = withPermission(
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
@@ -143,7 +143,7 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
title={`Are you sure you want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
@@ -156,7 +156,7 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.deactivateMember.isOpen}
title={`Are you sure want to deactivate member with username ${
title={`Are you sure you want to deactivate member with username ${
(popUp?.deactivateMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
@@ -280,7 +280,7 @@ export const OrgRoleTable = () => {
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteRole?.data as TOrgRole)?.name || " "
} role?`}
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
@@ -67,6 +67,15 @@ export const OrgAdminProjects = withPermission(
});
return;
}
if (type === ProjectType.SecretScanning) {
await navigate({
to: "/secret-scanning/$projectId/data-sources" as const,
params: {
projectId
}
});
return;
}
await navigate({
to: `/${type}/$projectId/overview` as const,
params: {
@@ -19,6 +19,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";
@@ -72,6 +73,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:
@@ -150,6 +153,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:
@@ -188,7 +193,6 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <OCIConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OnePass:
return <OnePassConnectionForm 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}
@@ -31,7 +31,7 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
return (
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => {
const { image, name, size = 50, enterprise = false } = APP_CONNECTION_MAP[option.app];
const { image, name, size = 50, enterprise = false, icon } = APP_CONNECTION_MAP[option.app];
return (
<button
@@ -43,14 +43,23 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
}
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="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
{icon && (
<FontAwesomeIcon
className="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>
@@ -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">
@@ -44,7 +44,7 @@ export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection }
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteAppConnection}
>
@@ -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) {
@@ -229,7 +229,7 @@ export const GatewayListPage = withPermission(
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure want to delete gateway ${
title={`Are you sure you want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
@@ -157,7 +157,7 @@ const Page = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
title={`Are you sure you want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
@@ -94,7 +94,7 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
<AddGroupMembersModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.removeMemberFromGroup.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeMemberFromGroup?.data as { username: string })?.username || ""
} from the group?`}
onChange={(isOpen) => handlePopUpToggle("removeMemberFromGroup", isOpen)}
@@ -101,7 +101,7 @@ const Page = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
@@ -66,7 +66,7 @@ export const IdentityProjectsSection = ({ identityId }: Props) => {
</div>
<DeleteActionModal
isOpen={popUp.removeIdentityFromProject.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeIdentityFromProject?.data as { identityName: string })?.identityName || ""
} from ${
(popUp?.removeIdentityFromProject?.data as { projectName: string })?.projectName || ""
@@ -218,7 +218,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
title={`Are you sure you want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
@@ -175,7 +175,7 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
title={`Are you sure you want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
@@ -151,7 +151,7 @@ export const Content = ({
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
title={`Are you sure you want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
@@ -155,7 +155,7 @@ export const Page = () => {
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteOrgRole.isOpen}
title={`Are you sure want to delete the organization role ${data?.name ?? ""}?`}
title={`Are you sure you want to delete the organization role ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrgRole", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onDeleteOrgRoleSubmit()}
@@ -24,6 +24,8 @@ const formatDescription = (type: ProjectType) => {
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
if (type === ProjectType.SecretScanning)
return "Connect and monitor data sources to prevent secret leaks.";
return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
};
@@ -0,0 +1,7 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const SecretScanningOverviewPage = () => (
<ProductOverviewPage type={ProjectType.SecretScanning} />
);
@@ -0,0 +1,23 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretScanningOverviewPage } from "./SecretScanningOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/overview"
)({
component: SecretScanningOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Scanning",
link: linkOptions({ to: "/organization/secret-scanning/overview" })
}
]
})
});
@@ -1,227 +0,0 @@
import { useEffect } from "react";
import { Helmet } from "react-helmet";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useServerConfig
} from "@app/context";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { withPermission } from "@app/hoc";
import { usePagination, usePopUp } from "@app/hooks";
import {
useCreateNewInstallationSession,
useGetSecretScanningInstallationStatus,
useGetSecretScanningRisks,
useLinkGitAppInstallationWithOrg
} from "@app/hooks/api/secretScanning";
import { SecretScanningOrderBy } from "@app/hooks/api/secretScanning/types";
import { ExportSecretScansModal } from "./components/ExportSecretScansModal";
import { SecretScanningFilter } from "./components/SecretScanningFilters";
import { SecretScanningFilterFormData, secretScanningFilterFormSchema } from "./components/types";
import { SecretScanningLogsTable } from "./components";
export const SecretScanningPage = withPermission(
() => {
const queryParams = useSearch({
from: ROUTE_PATHS.Organization.SecretScanning.id
});
const navigate = useNavigate();
const { control, watch } = useForm<SecretScanningFilterFormData>({
resolver: zodResolver(secretScanningFilterFormSchema),
defaultValues: {}
});
const { config } = useServerConfig();
const { currentOrg } = useOrganization();
const organizationId = currentOrg.id;
const { offset, limit, orderBy, setPage, perPage, page, setPerPage } = usePagination(
SecretScanningOrderBy.CreatedAt,
{ initPerPage: getUserTablePreference("secretScanningTable", PreferenceKey.PerPage, 20) }
);
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("secretScanningTable", PreferenceKey.PerPage, newPerPage);
};
const repositoryNames = watch("repositoryNames");
const resolvedStatus = watch("resolved");
const { isPending, data: risksData } = useGetSecretScanningRisks(
organizationId,
{
offset,
limit,
orderBy
},
{
repositoryNames:
repositoryNames?.length > 0 ? repositoryNames.map((repo) => repo.name) : undefined,
resolvedStatus
}
);
const { mutateAsync: linkGitAppInstallationWithOrganization } =
useLinkGitAppInstallationWithOrg();
const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
const {
data: installationStatus,
isPending: isSecretScanningInstatllationStatusLoading,
refetch: refetchSecretScanningInstallationStatus
} = useGetSecretScanningInstallationStatus(organizationId);
const integrationEnabled =
!isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
const { handlePopUpToggle, popUp } = usePopUp(["exportSecretScans"]);
useEffect(() => {
const linkInstallation = async () => {
if (queryParams.state && queryParams.installation_id) {
try {
const isLinked = await linkGitAppInstallationWithOrganization({
installationId: String(queryParams.installation_id),
sessionId: String(queryParams.state)
});
if (isLinked) {
await navigate({
to: "/organization/secret-scanning"
});
refetchSecretScanningInstallationStatus();
}
console.log("installation verification complete");
} catch (e) {
console.log("app installation is stale, start new session", e);
}
}
};
linkInstallation();
}, [queryParams.state, queryParams.installation_id]);
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession({ organizationId });
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
};
return (
<div className="py-2">
<Helmet>
<title>Secret scanning</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Secret Scanning"
description="Automatically monitor your GitHub activity and prevent secret leaks"
/>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased
usage.
</NoticeBanner>
)}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex flex-col items-start">
<div className="mb-1 flex flex-row">
Secret Scanning Status:{" "}
{integrationEnabled ? (
<p className="ml-1.5 font-semibold text-green">Enabled</p>
) : (
<p className="ml-1.5 font-semibold text-red">Not enabled</p>
)}
</div>
<div>
{integrationEnabled ? (
<p className="text-mineshaft-300">
Your GitHub organization is connected to Infisical, and is being continuously
monitored for secret leaks.
</p>
) : (
<p className="text-mineshaft-300">
Connect your GitHub organization to Infisical.
</p>
)}
</div>
</div>
{integrationEnabled ? (
<div>
<div className="absolute right-[2.5rem] top-[2.5rem] flex h-6 w-6 animate-ping items-center justify-center rounded-full bg-green" />
<div className="absolute right-[2.63rem] top-[2.63rem] flex h-5 w-5 animate-ping items-center justify-center rounded-full bg-green" />
<div className="absolute right-[2.82rem] top-[2.82rem] flex h-3.5 w-3.5 animate-ping items-center justify-center rounded-full bg-green" />
</div>
) : (
<div className="flex h-[3.25rem] items-center">
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.SecretScanning}
>
{(isAllowed) => (
<Button
variant="solid"
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="h-min py-2"
isDisabled={!isAllowed}
>
Integrate with GitHub
</Button>
)}
</OrgPermissionCan>
</div>
)}
</div>
<div className="mt-8 space-y-2">
{integrationEnabled && (
<div className="flex w-full items-center justify-end">
<SecretScanningFilter
repositories={risksData?.repos || []}
handlePopUpToggle={handlePopUpToggle}
control={control}
/>
</div>
)}
<SecretScanningLogsTable gitRisks={risksData?.risks} isPending={isPending} />
{!isPending && risksData?.totalCount !== undefined && risksData.totalCount >= 10 && (
<Pagination
className="rounded-md"
count={risksData.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</div>
</div>
<ExportSecretScansModal
repositories={risksData?.repos || []}
handlePopUpToggle={handlePopUpToggle}
popUp={popUp}
/>
</div>
);
},
{
action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.SecretScanning
}
);
@@ -1,200 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useExportSecretScanningRisks } from "@app/hooks/api/secretScanning";
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { convertJsonToCsv } from "@app/lib/fn/csv";
type Props = {
popUp: UsePopUpState<["exportSecretScans"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
state?: boolean
) => void;
repositories: string[];
};
enum ExportFormat {
Json = "json",
Csv = "csv"
}
const formSchema = z.object({
githubRepository: z.string().trim(),
status: z.nativeEnum(SecretScanningResolvedStatus),
exportFormat: z.nativeEnum(ExportFormat)
});
type TFormSchema = z.infer<typeof formSchema>;
export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, repositories }: Props) => {
const { currentOrg } = useOrganization();
const { mutateAsync } = useExportSecretScanningRisks();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
exportFormat: ExportFormat.Json,
status: SecretScanningResolvedStatus.All,
githubRepository: "all"
}
});
const onFormSubmit = async (data: TFormSchema) => {
const gitRisks = await mutateAsync({
orgId: currentOrg.id,
filter: {
repositoryNames: data.githubRepository !== "all" ? [data.githubRepository] : undefined,
resolvedStatus: data.status
}
});
const filteredRisks = gitRisks
.filter((risk) =>
// eslint-disable-next-line no-nested-ternary
data.status === SecretScanningResolvedStatus.All
? true
: data.status === SecretScanningResolvedStatus.Resolved
? risk.isResolved
: !risk.isResolved
)
.filter((risk) => {
if (data.githubRepository === "all") return true;
return risk.repositoryFullName === data.githubRepository;
});
const formattedRisks = filteredRisks.map((risk) => {
return {
repository: risk.repositoryFullName,
fileName: risk.file,
isResolved: risk.isResolved ? "Resolved" : "Needs Attention",
status: risk.status || "UNRESOLVED",
entropy: risk.entropy,
secretType: risk.ruleID,
exposedSecretLink: `https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`,
commitAuthor: risk.author,
commitAuthorEmail: risk.email,
commitDate: risk.createdAt,
commitMessage: risk.message,
commitFingerprint: risk.fingerprint
};
});
const fileName = `infisical-secret-scans-${new Date().toISOString()}`;
if (data.exportFormat === ExportFormat.Csv) {
const csvBlob = convertJsonToCsv(formattedRisks);
FileSaver.saveAs(csvBlob, `${fileName}.csv`);
} else if (data.exportFormat === ExportFormat.Json) {
const json = JSON.stringify(formattedRisks, null, 2);
const blob = new Blob([json], { type: "application/json" });
FileSaver.saveAs(blob, `${fileName}.json`);
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Modal
isOpen={popUp?.exportSecretScans?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("exportSecretScans", isOpen);
}}
>
<ModalContent
title="Export Secret Scans"
footerContent={[
<Button
onClick={() => handleSubmit(onFormSubmit)()}
isLoading={isSubmitting}
type="submit"
colorSchema="primary"
>
Export secret scans
</Button>,
<Button
key="keep-old-btn"
className="ml-4"
onClick={() => handlePopUpToggle("exportSecretScans", false)}
variant="outline_bg"
isDisabled={isSubmitting}
>
Cancel
</Button>
]}
>
<Controller
control={control}
name="exportFormat"
render={({ field: { value, onChange } }) => (
<FormControl label="Export Format">
<Select
defaultValue={ExportFormat.Json}
value={value}
onValueChange={onChange}
className="w-full"
>
{Object.values(ExportFormat).map((format) => (
<SelectItem key={format} value={format}>
{format.toUpperCase()}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="status"
render={({ field: { value, onChange } }) => (
<FormControl label="Risk Status">
<Select
defaultValue="all"
value={value}
onValueChange={onChange}
className="w-full"
>
{Object.values(SecretScanningResolvedStatus).map((status) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="githubRepository"
render={({ field: { value, onChange } }) => (
<FormControl label="GitHub Repository">
<Select
defaultValue="all"
value={value}
onValueChange={onChange}
className="w-full"
>
<SelectItem value="all">All Repositories</SelectItem>
{repositories.map((repoName) => (
<SelectItem key={repoName} value={repoName}>
{repoName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</ModalContent>
</Modal>
</form>
);
};
@@ -1,46 +0,0 @@
import { useState } from "react";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateRiskStatus } from "@app/hooks/api/secretScanning";
import { RiskStatus } from "@app/hooks/api/secretScanning/types";
export const RiskStatusSelection = ({
riskId,
currentSelection
}: {
riskId: any;
currentSelection: any;
}) => {
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
const { mutate: updateRiskStatus } = useUpdateRiskStatus();
const { currentOrg } = useOrganization();
const organizationId = currentOrg.id;
return (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
{(isAllowed) => (
<select
disabled={!isAllowed}
value={selectedRiskStatus}
onChange={(e) => {
setSelectedRiskStatus(e.target.value);
updateRiskStatus({
organizationId,
status: e.target.value as RiskStatus,
riskId
});
}}
className="block w-full rounded-md px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
>
<option>Unresolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>
This is a false positive, resolved
</option>
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
</select>
)}
</OrgPermissionCan>
);
};
@@ -1,89 +0,0 @@
import { Control, Controller } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { SecretScanningFilterFormData } from "./types";
type Props = {
control: Control<SecretScanningFilterFormData>;
repositories: string[];
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
state?: boolean
) => void;
};
export const SecretScanningFilter = ({ repositories, control, handlePopUpToggle }: Props) => {
return (
<div className={twMerge("flex w-full flex-wrap items-center justify-between bg-bunker-800")}>
<div className="flex items-center -space-x-8">
<Controller
control={control}
name="repositoryNames"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Repository"
errorText={error?.message}
isError={Boolean(error)}
className="mr-12 w-96"
>
<FilterableSelect
value={value}
isClearable
isMulti
onChange={onChange}
placeholder="Select a repository..."
options={repositories.map((repository) => ({
name: repository
}))}
getOptionValue={(option) => option.name}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="resolved"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Status"
errorText={error?.message}
isError={Boolean(error)}
className="mr-12 w-44"
>
<Select
defaultValue={SecretScanningResolvedStatus.All}
placeholder={SecretScanningResolvedStatus.All}
value={value}
onValueChange={onChange}
className="w-full"
>
{Object.values(SecretScanningResolvedStatus).map((status) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)} risks
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div>
<Button
className="mt-[0.45rem]"
onClick={() => handlePopUpToggle("exportSecretScans", true)}
variant="solid"
colorSchema="secondary"
>
Export
</Button>
</div>
</div>
);
};
@@ -1,91 +0,0 @@
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { formatDistance } from "date-fns";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { TSecretScanningGitRisks } from "@app/hooks/api/secretScanning/types";
import { RiskStatusSelection } from "./RiskStatusSelection";
type Props = {
gitRisks?: TSecretScanningGitRisks[];
isPending: boolean;
};
export const SecretScanningLogsTable = ({ gitRisks, isPending }: Props) => {
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Secret Type</Th>
<Th className="flex-1">View Risk</Th>
<Th className="flex-1">Info</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Action</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isPending &&
gitRisks &&
gitRisks?.map((risk) => {
return (
<Tr key={risk.ruleID} className="h-10">
<Td>{formatDistance(new Date(risk.createdAt), new Date())}</Td>
<Td>{risk.ruleID}</Td>
<Td>
<a
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
target="_blank"
className="text-red-500"
rel="noreferrer"
>
View Exposed Secret
</a>
</Td>
<Td>
<div className="font-bold">
<a href={`https://github.com/${risk.repositoryFullName}`}>
{risk.repositoryFullName}
</a>
</div>
<div className="text-xs">
<span>{risk.file}</span>
<br />
<br />
<span className="font-bold">{risk.author}</span>
<br />
<span>{risk.email}</span>
</div>
</Td>
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
<Td>
<RiskStatusSelection riskId={risk.id} currentSelection={risk.status} />
</Td>
</Tr>
);
})}
{isPending && <TableSkeleton columns={7} innerKey="gitRisks" />}
{!isPending && gitRisks && gitRisks?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No risks detected." icon={faCheck} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -1 +0,0 @@
export { SecretScanningLogsTable } from "./SecretScanningLogsTable";
@@ -1,22 +0,0 @@
import { z } from "zod";
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
export const secretScanningFilterFormSchema = z.object({
repositoryNames: z.array(z.object({ name: z.string() })),
resolved: z
.nativeEnum(SecretScanningResolvedStatus)
.default(SecretScanningResolvedStatus.All)
.optional()
});
export type SecretScanningFilterFormData = z.infer<typeof secretScanningFilterFormSchema>;
export type SetValueType = (
name: keyof SecretScanningFilterFormData,
value: any,
options?: {
shouldValidate?: boolean;
shouldDirty?: boolean;
}
) => void;
@@ -1,39 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretScanningPage } from "./SecretScanningPage";
const SecretScanningQueryParams = z.object({
state: z.string().catch(""),
installation_id: z.coerce.string().optional()
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
)({
validateSearch: zodValidator(SecretScanningQueryParams),
component: SecretScanningPage,
search: {
middlewares: [
stripSearchParams({
installation_id: "",
state: ""
})
]
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Scanning"
}
]
})
});
@@ -0,0 +1,20 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const SecretScanningSettingsPage = () => {
return (
<>
<Helmet>
<title>Secret Scanning Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Secret Scanning Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};
@@ -0,0 +1,26 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretScanningSettingsPage } from "./SecretScanningSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/settings"
)({
component: SecretScanningSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Scanning",
link: linkOptions({ to: "/organization/secret-scanning/overview" })
},
{
label: "Settings"
}
]
})
});
@@ -59,7 +59,7 @@ export const OrgDeleteSection = () => {
</div>
<DeleteActionModal
isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?"
title="Are you sure you want to delete this organization?"
subTitle={`Permanently remove ${currentOrg?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteOrg", isOpen)}
deleteKey="confirm"
@@ -136,7 +136,7 @@ export const OrgEncryptionTab = withPermission(
/>
<DeleteActionModal
isOpen={popUp.removeExternalKms.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeExternalKms?.data as { slug: string })?.slug || ""
} from ${(popUp?.removeExternalKms?.data as { provider: string })?.provider || ""}?`}
onChange={(isOpen) => handlePopUpToggle("removeExternalKms", isOpen)}
@@ -305,7 +305,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
/>
<DeleteActionModal
isOpen={popUp.removeIntegration.isOpen}
title={`Are you sure want to remove ${popUp?.removeIntegration?.data?.slug}?`}
title={`Are you sure you want to remove ${popUp?.removeIntegration?.data?.slug}?`}
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveIntegration}
@@ -162,7 +162,7 @@ export const GithubOrgSyncConfigModal = ({
</form>
<DeleteActionModal
isOpen={popUp.deleteGithubOrgSyncConfig.isOpen}
title="Are you sure want to remove GitHub organization sync?"
title="Are you sure you want to remove GitHub organization sync?"
onChange={(isOpen) => handlePopUpToggle("deleteGithubOrgSyncConfig", isOpen)}
deleteKey="confirm"
onDeleteApproved={onDelete}
@@ -301,7 +301,7 @@ export const ScimTokenModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pr
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteScimToken.isOpen}
title="Are you sure want to delete the SCIM token?"
title="Are you sure you want to delete the SCIM token?"
onChange={(isOpen) => handlePopUpToggle("scimToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
@@ -249,7 +249,7 @@ export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }:
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteLdapGroupMap.isOpen}
title={`Are you sure want to delete the group mapping for ${
title={`Are you sure you want to delete the group mapping for ${
(popUp?.deleteLdapGroupMap?.data as { ldapGroupCN: string })?.ldapGroupCN || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteLdapGroupMap", isOpen)}
@@ -361,7 +361,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
</Modal>
<DeleteActionModal
isOpen={isDeletePopupOpen}
title="Are you sure want to delete LDAP?"
title="Are you sure you want to delete LDAP?"
onChange={() => setIsDeletePopupOpen.toggle()}
deleteKey="confirm"
onDeleteApproved={handleLdapSoftDelete}
@@ -478,7 +478,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
</Modal>
<DeleteActionModal
isOpen={isDeletePopupOpen}
title="Are you sure want to delete OIDC?"
title="Are you sure you want to delete OIDC?"
onChange={() => setIsDeletePopupOpen.toggle()}
deleteKey="confirm"
onDeleteApproved={handleOidcSoftDelete}
@@ -371,7 +371,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet
</Modal>
<DeleteActionModal
isOpen={isDeletePopupOpen}
title="Are you sure want to delete SAML SSO?"
title="Are you sure you want to delete SAML SSO?"
onChange={() => setIsDeletePopupOpen.toggle()}
deleteKey="confirm"
onDeleteApproved={handleSamlSoftDelete}
@@ -259,7 +259,7 @@ const Page = withPermission(
)}
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
title={`Are you sure you want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
@@ -272,7 +272,7 @@ const Page = withPermission(
/>
<DeleteActionModal
isOpen={popUp.deactivateMember.isOpen}
title={`Are you sure want to deactivate member with username ${
title={`Are you sure you want to deactivate member with username ${
(popUp?.deactivateMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
@@ -53,7 +53,7 @@ export const UserGroupsSection = ({ orgMembership }: Props) => {
<DeleteActionModal
isOpen={popUp.removeUserFromGroup.isOpen}
title="Are you sure want to unassign user from group?"
title="Are you sure you want to unassign user from group?"
onChange={(isOpen) => handlePopUpToggle("removeUserFromGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {