diff --git a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts new file mode 100644 index 000000000..c8257b771 --- /dev/null +++ b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues"))) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.specificType("secretDetectionIgnoreValues", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues")) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("secretDetectionIgnoreValues"); + }); + } +} diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 059565a94..08dc1eee0 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -30,7 +30,8 @@ export const ProjectsSchema = z.object({ hasDeleteProtection: z.boolean().default(false).nullable().optional(), secretSharing: z.boolean().default(true), showSnapshotsLegacy: z.boolean().default(false), - defaultProduct: z.string().nullable().optional() + defaultProduct: z.string().nullable().optional(), + secretDetectionIgnoreValues: z.string().array().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 8e366fefc..d1eefe7e3 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -69,6 +69,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; +import { scanSecretPolicyViolations } from "../secret-scanning-v2/secret-scanning-v2-fns"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; import { sendApprovalEmailsFn } from "./secret-approval-request-fns"; @@ -1412,6 +1413,20 @@ export const secretApprovalRequestServiceFactory = ({ projectId }); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + projectId, + secretPath, + [ + ...(data[SecretOperations.Create] || []), + ...(data[SecretOperations.Update] || []).filter((el) => el.secretValue) + ].map((el) => ({ + secretKey: el.secretKey, + secretValue: el.secretValue as string + })), + project.secretDetectionIgnoreValues || [] + ); + // for created secret approval change const createdSecrets = data[SecretOperations.Create]; if (createdSecrets && createdSecrets?.length) { diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 9489f4658..bdda63f7a 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -1,11 +1,21 @@ import { AxiosError } from "axios"; import { exec } from "child_process"; +import { join } from "path"; +import picomatch from "picomatch"; import RE2 from "re2"; -import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; +import { + createTempFolder, + deleteTempFolder, + readFindingsFile, + writeTextToFile +} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; import { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github"; +import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; +import { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums"; @@ -46,6 +56,19 @@ export function scanDirectory(inputPath: string, outputPath: string, configPath? }); } +export function scanFile(inputPath: string): Promise { + return new Promise((resolve, reject) => { + const command = `infisical scan --exit-code=77 --source "${inputPath}" --no-git`; + exec(command, (error) => { + if (error && error.code === 77) { + reject(error); + } else { + resolve(); + } + }); + }); +} + export const scanGitRepositoryAndGetFindings = async ( scanPath: string, findingsPath: string, @@ -140,3 +163,47 @@ export const parseScanErrorMessage = (err: unknown): string => { ? errorMessage : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; }; + +export const scanSecretPolicyViolations = async ( + projectId: string, + secretPath: string, + secrets: { secretKey: string; secretValue: string }[], + ignoreValues: string[] +) => { + const appCfg = getConfig(); + + if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) { + return; + } + + const match = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.find( + (el) => el.projectId === projectId && picomatch.isMatch(secretPath, el.secretPath, { strictSlashes: false }) + ); + + if (!match) { + return; + } + + const tempFolder = await createTempFolder(); + try { + const scanPromises = secrets + .filter((secret) => !ignoreValues.includes(secret.secretValue)) + .map(async (secret) => { + const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`); + await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); + + try { + await scanFile(secretFilePath); + } catch (error) { + throw new BadRequestError({ + message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`, + name: "SecretPolicyViolation" + }); + } + }); + + await Promise.all(scanPromises); + } finally { + await deleteTempFolder(tempFolder); + } +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 2bdbe3331..71b6afb6b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -704,7 +704,8 @@ export const PROJECTS = { hasDeleteProtection: "Enable or disable delete protection for the project.", secretSharing: "Enable or disable secret sharing for the project.", showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.", - defaultProduct: "The default product in which the project will open" + defaultProduct: "The default product in which the project will open", + secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection." }, GET_KEY: { workspaceId: "The ID of the project to get the key from." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 29f21ae20..3fa2c0f82 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -204,6 +204,17 @@ const envSchema = z WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), + // Special Detection Feature + PARAMS_FOLDER_SECRET_DETECTION_PATHS: zpStr( + z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + return JSON.parse(val) as { secretPath: string; projectId: string }[]; + }) + ), + // HSM HSM_LIB_PATH: zpStr(z.string().optional()), HSM_PIN: zpStr(z.string().optional()), @@ -358,6 +369,7 @@ const envSchema = z Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","), + PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b2b0c9924..0fb3191f8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1238,6 +1238,7 @@ export const registerRoutes = async ( const secretV2BridgeService = secretV2BridgeServiceFactory({ folderDAL, + projectDAL, secretVersionDAL: secretVersionV2BridgeDAL, folderCommitService, secretQueueService, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index aba8663a3..8af4baa8b 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -271,7 +271,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ auditLogsRetentionDays: true, hasDeleteProtection: true, secretSharing: true, - showSnapshotsLegacy: true + showSnapshotsLegacy: true, + secretDetectionIgnoreValues: true }); export const SanitizedTagSchema = SecretTagsSchema.pick({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6cc50dc5c..57aa42fae 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -52,7 +52,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { defaultAuthOrgAuthEnforced: z.boolean().nullish(), defaultAuthOrgAuthMethod: z.string().nullish(), isSecretScanningDisabled: z.boolean(), - kubernetesAutoFetchServiceAccountToken: z.boolean() + kubernetesAutoFetchServiceAccountToken: z.boolean(), + paramsFolderSecretDetectionEnabled: z.boolean() }) }) } @@ -67,7 +68,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { fipsEnabled: crypto.isFipsModeEnabled(), isMigrationModeOn: serverEnvs.MAINTENANCE_MODE, isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING, - kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN + kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN, + paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED } }; } diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 05aade960..6f981f61f 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -369,7 +369,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .describe(PROJECTS.UPDATE.slug), secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy), - defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct) + defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct), + secretDetectionIgnoreValues: z + .array(z.string()) + .optional() + .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues) }), response: { 200: z.object({ @@ -392,7 +396,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { hasDeleteProtection: req.body.hasDeleteProtection, slug: req.body.slug, secretSharing: req.body.secretSharing, - showSnapshotsLegacy: req.body.showSnapshotsLegacy + showSnapshotsLegacy: req.body.showSnapshotsLegacy, + secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 27925c4b9..153f627fc 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -645,7 +645,7 @@ export const projectServiceFactory = ({ const updateProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, update, filter }: TUpdateProjectDTO) => { const project = await projectDAL.findProjectByFilter(filter); - const { permission } = await permissionService.getProjectPermission({ + const { permission, hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -667,6 +667,12 @@ export const projectServiceFactory = ({ } } + if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ + message: "Only admins can update secret detection ignore values" + }); + } + const updatedProject = await projectDAL.updateById(project.id, { name: update.name, description: update.description, @@ -676,7 +682,8 @@ export const projectServiceFactory = ({ slug: update.slug, secretSharing: update.secretSharing, defaultProduct: update.defaultProduct, - showSnapshotsLegacy: update.showSnapshotsLegacy + showSnapshotsLegacy: update.showSnapshotsLegacy, + secretDetectionIgnoreValues: update.secretDetectionIgnoreValues }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index b8c37a858..02f89bc38 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -96,6 +96,7 @@ export type TUpdateProjectDTO = { slug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; + secretDetectionIgnoreValues?: string[]; }; } & Omit; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 2fa0ffe9b..43daaf0df 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -25,6 +25,7 @@ import { import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; +import { scanSecretPolicyViolations } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -38,6 +39,7 @@ import { ActorType } from "../auth/auth-type"; import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; @@ -88,6 +90,7 @@ import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal"; type TSecretV2BridgeServiceFactoryDep = { secretDAL: TSecretV2BridgeDALFactory; + projectDAL: Pick; secretVersionDAL: TSecretVersionV2DALFactory; kmsService: Pick; secretVersionTagDAL: Pick; @@ -126,6 +129,7 @@ export type TSecretV2BridgeServiceFactory = ReturnType ({ secretKey: el, secretPath, environment })) @@ -506,6 +523,21 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, secretValue } = inputSecret; + if (secretValue) { + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + projectId, + secretPath, + [ + { + secretKey: inputSecret.newSecretName || secretName, + secretValue + } + ], + project.secretDetectionIgnoreValues || [] + ); + } + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId @@ -1585,6 +1617,9 @@ export const secretV2BridgeServiceFactory = ({ if (secrets.length) throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []); + // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; @@ -1925,6 +1960,19 @@ export const secretV2BridgeServiceFactory = ({ }); await $validateSecretReferences(projectId, permission, secretReferences, tx); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + projectId, + secretPath, + secretsToUpdate + .filter((el) => el.secretValue) + .map((el) => ({ + secretKey: el.newSecretName || el.secretKey, + secretValue: el.secretValue as string + })), + project.secretDetectionIgnoreValues || [] + ); + const bulkUpdatedSecrets = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 6685e5b77..51ff5a908 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -51,6 +51,7 @@ export type TServerConfig = { invalidatingCache: boolean; fipsEnabled: boolean; envOverrides?: Record; + paramsFolderSecretDetectionEnabled: boolean; }; export type TUpdateServerConfigDTO = { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 6408d0e22..7a0e3d2e2 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -281,7 +281,8 @@ export const useUpdateProject = () => { newProjectDescription, newSlug, secretSharing, - showSnapshotsLegacy + showSnapshotsLegacy, + secretDetectionIgnoreValues }) => { const { data } = await apiRequest.patch<{ workspace: Workspace }>( `/api/v1/workspace/${projectID}`, @@ -290,7 +291,8 @@ export const useUpdateProject = () => { description: newProjectDescription, slug: newSlug, secretSharing, - showSnapshotsLegacy + showSnapshotsLegacy, + secretDetectionIgnoreValues } ); return data.workspace; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 7d6f68821..33eb0909f 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -40,6 +40,7 @@ export type Workspace = { hasDeleteProtection: boolean; secretSharing: boolean; showSnapshotsLegacy: boolean; + secretDetectionIgnoreValues: string[]; }; export type WorkspaceEnv = { @@ -81,6 +82,7 @@ export type UpdateProjectDTO = { newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; + secretDetectionIgnoreValues?: string[]; }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index 5f817a46a..a542facf5 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -1,12 +1,17 @@ +import { useServerConfig } from "@app/context"; + import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection"; import { EnvironmentSection } from "../EnvironmentSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; +import { SecretDetectionIgnoreValuesSection } from "../SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection"; import { SecretSharingSection } from "../SecretSharingSection"; import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection"; import { SecretTagsSection } from "../SecretTagsSection"; export const SecretSettingsTab = () => { + const { config } = useServerConfig(); + return (
@@ -15,6 +20,7 @@ export const SecretSettingsTab = () => { + {config.paramsFolderSecretDetectionEnabled && }
); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx new file mode 100644 index 000000000..48d0e69e8 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -0,0 +1,144 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } 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 { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; + +const formSchema = z.object({ + ignoreValues: z + .object({ + value: z.string().trim().min(1, "Secret value is required") + }) + .array() + .default([]) +}); + +type TForm = z.infer; + +export const SecretDetectionIgnoreValuesSection = () => { + const { currentWorkspace } = useWorkspace(); + const { membership } = useProjectPermission(); + const { mutateAsync: updateProject } = useUpdateProject(); + + const { + control, + formState: { isSubmitting, isDirty }, + handleSubmit, + reset + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + ignoreValues: [] + } + }); + + const ignoreValuesFormFields = useFieldArray({ + control, + name: "ignoreValues" + }); + + useEffect(() => { + const existingIgnoreValues = currentWorkspace?.secretDetectionIgnoreValues || []; + reset({ + ignoreValues: + existingIgnoreValues.length > 0 + ? existingIgnoreValues.map((value) => ({ value })) + : [{ value: "" }] // Show one empty field by default + }); + }, [currentWorkspace?.secretDetectionIgnoreValues, reset]); + + const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => { + try { + await updateProject({ + projectID: currentWorkspace.id, + secretDetectionIgnoreValues: ignoreValues.map((item) => item.value) + }); + + createNotification({ + text: "Successfully updated secret detection ignore values", + type: "success" + }); + } catch { + createNotification({ + text: "Failed updating secret detection ignore values", + type: "error" + }); + } + }; + + const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + + if (!currentWorkspace) return null; + + return ( +
+
+

Secret Detection

+
+

Define secret values to ignore when scanning designated parameter folders. Add values here to prevent false positives or allow approved sensitive data. These ignored values will not trigger policy violation alerts.

+ +
+
+
+ {ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => ( +
+
+ {i === 0 && Secret Value} + ( + + + + )} + /> +
+ ignoreValuesFormFields.remove(i)} + isDisabled={!isAdmin} + > + + +
+ ))} +
+ +
+
+
+ + +
+
+ ); +};