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
@@ -128,6 +128,11 @@ export enum ProjectPermissionSecretScanningFindingActions {
Update = "update-findings"
}
export enum ProjectPermissionSecretScanningConfigActions {
Read = "read-configs",
Update = "update-configs"
}
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
@@ -216,7 +221,8 @@ export enum ProjectPermissionSub {
SecretSyncs = "secret-syncs",
Kmip = "kmip",
SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings"
SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs"
}
export type SecretSubjectFields = {
@@ -345,6 +351,7 @@ export type ProjectPermissionSet =
ProjectPermissionSecretScanningDataSourceActions,
ProjectPermissionSub.SecretScanningDataSources
]
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings];
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
@@ -201,7 +201,9 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.SECRET_SCANNING_RESOURCE_LIST]: "List Secret Scanning Resources",
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
[EventType.SECRET_SCANNING_FINDING_LIST]: "List Secret Scanning Findings",
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status"
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status",
[EventType.SECRET_SCANNING_CONFIG_GET]: "Get Secret Scanning Configuration",
[EventType.SECRET_SCANNING_CONFIG_UPDATE]: "Update Secret Scanning Configuration"
};
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
+3 -1
View File
@@ -195,5 +195,7 @@ export enum EventType {
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update"
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update"
}
@@ -6,6 +6,8 @@ import { secretScanningV2Keys } from "./queries";
import {
TCreateSecretScanningDataSourceDTO,
TDeleteSecretScanningDataSourceDTO,
TGetSecretScanningConfigResponse,
TSecretScanningConfig,
TSecretScanningDataSourceResponse,
TSecretScanningFindingResponse,
TTriggerSecretScanningDataSourceDTO,
@@ -128,7 +130,31 @@ export const useUpdateSecretScanningFinding = () => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listFindings(projectId)
});
// TODO: data source queries
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.findingCount(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSource()
});
}
});
};
export const useUpdateSecretScanningConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
`/api/v2/secret-scanning/configs/${projectId}`,
params
);
return data.config;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.configByProjectId(projectId)
});
}
});
};
@@ -3,6 +3,7 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TGetSecretScanningConfigResponse,
TGetSecretScanningDataSource,
TGetSecretScanningUnresolvedFindingsResponse,
TListSecretScanningDataSourceOptions,
@@ -10,6 +11,7 @@ import {
TListSecretScanningFindingsResponse,
TListSecretScanningResourcesResponse,
TListSecretScanningScansResponse,
TSecretScanningConfig,
TSecretScanningDataSource,
TSecretScanningDataSourceOption,
TSecretScanningDataSourceResponse,
@@ -42,7 +44,9 @@ export const secretScanningV2Keys = {
finding: () => [...secretScanningV2Keys.all, "finding"] as const,
findingCount: (projectId: string) =>
[...secretScanningV2Keys.finding(), "count", projectId] as const,
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId]
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId],
configByProjectId: (projectId: string) =>
[...secretScanningV2Keys.all, "config", projectId] as const
};
export const useSecretScanningDataSourceOptions = (
@@ -216,3 +220,28 @@ export const useListSecretScanningFindings = (
...options
});
};
export const useGetSecretScanningConfig = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningConfig,
unknown,
TSecretScanningConfig,
ReturnType<typeof secretScanningV2Keys.configByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.configByProjectId(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
`/api/v2/secret-scanning/configs/${projectId}`
);
return data.config;
},
...options
});
};
@@ -115,6 +115,15 @@ export type TGetSecretScanningUnresolvedFindingsResponse = {
unresolvedFindings: number;
};
export type TSecretScanningConfig = {
projectId: string;
content: string | null;
};
export type TGetSecretScanningConfigResponse = {
config: TSecretScanningConfig;
};
export type TSecretScanningFinding = {
id: string;
dataSourceName: string;
@@ -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";