feature: secret scanning pt 3

This commit is contained in:
Scott Wilson
2025-05-30 20:19:44 -07:00
parent 4773336a04
commit ce4e35e908
37 changed files with 946 additions and 102 deletions
@@ -20,6 +20,7 @@ import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions,
ProjectPermissionSecretScanningConfigActions,
ProjectPermissionSecretScanningDataSourceActions,
ProjectPermissionSecretScanningFindingActions,
ProjectPermissionSecretSyncActions,
@@ -107,6 +108,11 @@ const SecretScanningFindingPolicyActionSchema = z.object({
[ProjectPermissionSecretScanningFindingActions.Update]: z.boolean().optional()
});
const SecretScanningConfigPolicyActionSchema = z.object({
[ProjectPermissionSecretScanningConfigActions.Read]: z.boolean().optional(),
[ProjectPermissionSecretScanningConfigActions.Update]: z.boolean().optional()
});
const KmipPolicyActionSchema = z.object({
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
@@ -295,7 +301,9 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.SecretScanningDataSources]:
SecretScanningDataSourcePolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretScanningFindings]:
SecretScanningFindingPolicyActionSchema.array().default([])
SecretScanningFindingPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretScanningConfigs]:
SecretScanningConfigPolicyActionSchema.array().default([])
})
.partial()
.optional()
@@ -777,6 +785,18 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
formVal[subject]![0][ProjectPermissionSecretScanningFindingActions.Update] = true;
}
if (subject === ProjectPermissionSub.SecretScanningConfigs) {
const canRead = action.includes(ProjectPermissionSecretScanningConfigActions.Read);
const canUpdate = action.includes(ProjectPermissionSecretScanningConfigActions.Update);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0][ProjectPermissionSecretScanningConfigActions.Read] = true;
if (canUpdate)
formVal[subject]![0][ProjectPermissionSecretScanningConfigActions.Update] = true;
}
if (subject === ProjectPermissionSub.SshHosts) {
if (!formVal[subject]) formVal[subject] = [];
@@ -1363,6 +1383,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
value: ProjectPermissionSecretScanningFindingActions.Update
}
]
},
[ProjectPermissionSub.SecretScanningConfigs]: {
title: "Secret Scanning Config",
actions: [
{
label: "Read Config",
value: ProjectPermissionSecretScanningConfigActions.Read
},
{
label: "Update Config",
value: ProjectPermissionSecretScanningConfigActions.Update
}
]
}
};
@@ -1418,7 +1451,8 @@ const SshPermissionSubjects = (enabled = false) => ({
const SecretScanningSubject = (enabled = false) => ({
[ProjectPermissionSub.SecretScanningDataSources]: enabled,
[ProjectPermissionSub.SecretScanningFindings]: enabled
[ProjectPermissionSub.SecretScanningFindings]: enabled,
[ProjectPermissionSub.SecretScanningConfigs]: enabled
});
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
@@ -1693,6 +1727,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
{
subject: ProjectPermissionSub.SecretScanningFindings,
actions: [ProjectPermissionSecretScanningFindingActions.Read]
},
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: [ProjectPermissionSecretScanningConfigActions.Read]
}
]
},
@@ -1708,10 +1746,19 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
{
subject: ProjectPermissionSub.SecretScanningFindings,
actions: Object.values(ProjectPermissionSecretScanningFindingActions)
},
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: [ProjectPermissionSecretScanningConfigActions.Read]
}
]
},
projectManagerTemplate()
projectManagerTemplate([
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: Object.values(ProjectPermissionSecretScanningConfigActions)
}
])
],
[ProjectType.SecretManager]: [
{
@@ -114,7 +114,7 @@ export const SecretScanningScanRow = ({ scan }: Props) => {
<span className="text-xs">
{totalFindings}{" "}
{unresolvedFindings
? `Secret ${totalFindings > 1 ? "s" : ""} Detected`
? `Secret${totalFindings > 1 ? "s" : ""} Detected`
: "Leak Resolved"}
</span>
</Badge>
@@ -3,9 +3,11 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab";
export const SettingsPage = () => {
const { t } = useTranslation();
@@ -21,26 +23,29 @@ export const SettingsPage = () => {
<TabList>
<Tab value="tab-project-general">General</Tab>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
I={ProjectPermissionSecretScanningConfigActions.Read}
a={ProjectPermissionSub.SecretScanningConfigs}
>
{(isAllowed) =>
isAllowed && <Tab value="tab-project-secret-scannings">Rule Settings</Tab>
isAllowed && <Tab value="tab-project-scanning-config">Scanning Configuration</Tab>
}
</ProjectPermissionCan>
</TabList>
<TabPanel value="tab-project-general">
<ProjectGeneralTab />
</TabPanel>
{/* <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningConfigActions.Read}
a={ProjectPermissionSub.SecretScanningConfigs}
>
{(isAllowed) =>
isAllowed && (
<TabPanel value="tab-project-ssh">
<ProjectSshTab />
<TabPanel value="tab-project-scanning-config">
<ProjectScanningConfigTab />
</TabPanel>
)
}
</ProjectPermissionCan> */}
</ProjectPermissionCan>
</Tabs>
</div>
</div>
@@ -0,0 +1,40 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { ContentLoader, EmptyState } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2";
import { SecretScanningConfigForm } from "./SecretScanningConfigForm";
export const ProjectScanningConfigTab = () => {
const { currentWorkspace } = useWorkspace();
const { data: config, isPending: isConfigPending } = useGetSecretScanningConfig(
currentWorkspace.id
);
if (isConfigPending) {
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader />
</div>
);
}
if (!config) {
return (
<div className="flex h-full w-full items-center justify-center px-20">
<EmptyState
className="max-w-2xl rounded-md text-center"
icon={faBan}
title="Could not find Project Configuration"
/>
</div>
);
}
return (
<div>
<SecretScanningConfigForm config={config} />
</div>
);
};
@@ -0,0 +1,132 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, TextArea } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
import {
TSecretScanningConfig,
useUpdateSecretScanningConfig
} from "@app/hooks/api/secretScanningV2";
type Props = {
config: TSecretScanningConfig;
};
const FormSchema = z.object({
content: z.string().max(5000, "Configuration cannot exceed 5000 characters").optional()
});
type FormType = z.infer<typeof FormSchema>;
export const SecretScanningConfigForm = ({ config }: Props) => {
const updateConfig = useUpdateSecretScanningConfig();
const {
handleSubmit,
control,
formState: { isDirty }
} = useForm<FormType>({
resolver: zodResolver(FormSchema),
defaultValues: {
content: config.content ?? ""
}
});
const onSubmit = async ({ content }: FormType) => {
try {
await updateConfig.mutateAsync({
projectId: config.projectId,
content: content || null
});
createNotification({
type: "success",
text: "Configuration successfully updated"
});
} catch {
createNotification({
type: "error",
text: "Failed to update Configuration"
});
}
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold text-mineshaft-100">Project Configuration</h2>
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview#configuration"
className="mt-[0.02rem]"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="leading-5 text-mineshaft-400">
Configure rules and exceptions to customize scanning
</p>
</div>
<div>
<form onSubmit={handleSubmit(onSubmit)} className="flex w-full flex-col">
<ProjectPermissionCan
I={ProjectPermissionSecretScanningConfigActions.Update}
a={ProjectPermissionSub.SecretScanningConfigs}
>
{(isAllowed) => (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<TextArea
placeholder={
"[extend]\n\nuseDefault = true\n\n# See docs for configuration guide"
}
{...field}
rows={3}
className="thin-scrollbar min-h-[36rem] w-full !resize-none !resize-y"
isDisabled={!isAllowed}
/>
</FormControl>
)}
control={control}
name="content"
/>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningConfigActions.Update}
a={ProjectPermissionSub.SecretScanningConfigs}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
className="w-min"
isLoading={updateConfig.isPending}
isDisabled={updateConfig.isPending || !isAllowed || !isDirty}
>
Save
</Button>
)}
</ProjectPermissionCan>
</form>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { ProjectScanningConfigTab } from "./ProjectScanningConfigTab";