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 0000000000..c8257b7712 --- /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 059565a944..08dc1eee03 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 8e366fefc2..d1eefe7e36 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 9489f46589..bdda63f7a5 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 2bdbe33310..71b6afb6bc 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 29f21ae20c..3fa2c0f823 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 b2b0c9924c..0fb3191f89 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 aba8663a36..8af4baa8bb 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 6cc50dc5c0..57aa42fae6 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 05aade960e..6f981f61f1 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 27925c4b96..153f627fcb 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 b8c37a858c..02f89bc38b 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 2fa0ffe9bf..43daaf0dfc 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 6685e5b777..51ff5a9083 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 6408d0e223..7a0e3d2e23 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 7d6f68821e..33eb0909f9 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 5f817a46af..a542facf5e 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 0000000000..48d0e69e85 --- /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} + > + + +
+ ))} +
+ +
+
+
+ + +
+
+ ); +};