From c368178cb166e10040016f6483eeea284a735834 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 26 Jul 2025 03:00:44 +0800 Subject: [PATCH 1/5] feat: secrets detection in secret manager --- ...171821_add-secret-detection-ignore-keys.ts | 19 +++ backend/src/db/schemas/projects.ts | 3 +- .../secret-scanning-v2-fns.ts | 69 +++++++- backend/src/lib/api-docs/constants.ts | 3 +- backend/src/lib/config/env.ts | 12 ++ backend/src/server/routes/index.ts | 1 + backend/src/server/routes/sanitizedSchemas.ts | 3 +- backend/src/server/routes/v1/admin-router.ts | 6 +- .../src/server/routes/v1/project-router.ts | 6 +- .../src/services/project/project-service.ts | 11 +- backend/src/services/project/project-types.ts | 1 + .../secret-v2-bridge-service.ts | 45 ++++++ frontend/src/hooks/api/admin/types.ts | 1 + frontend/src/hooks/api/workspace/queries.tsx | 6 +- frontend/src/hooks/api/workspace/types.ts | 2 + .../ProjectGeneralTab/ProjectGeneralTab.tsx | 6 + .../SecretDetectionIgnoreKeysSection.tsx | 147 ++++++++++++++++++ 17 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts create mode 100644 frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx diff --git a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts new file mode 100644 index 000000000..dfdf5ecfc --- /dev/null +++ b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.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, "secretDetectionIgnoreKeys"))) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.specificType("secretDetectionIgnoreKeys", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys")) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("secretDetectionIgnoreKeys"); + }); + } +} diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 059565a94..fd8b273f2 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(), + secretDetectionIgnoreKeys: z.string().array().nullable().optional() }); export type TProjects = z.infer; 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..917f992d3 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,20 @@ 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 { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums"; @@ -46,6 +55,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 +162,48 @@ export const parseScanErrorMessage = (err: unknown): string => { ? errorMessage : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; }; + +export const scanSecretPolicyViolations = async ( + secretPath: string, + secrets: { secretKey: string; secretValue: string }[], + ignoreKeys: string[] +) => { + const appCfg = getConfig(); + + if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) { + return; + } + const paramFolderSecretDetectionPaths = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.map((el) => el.secretPath) ?? []; + const isPathMatched = paramFolderSecretDetectionPaths.some((pattern) => + picomatch.isMatch(secretPath, pattern, { strictSlashes: false }) + ); + + if (!isPathMatched) { + return; + } + + const tempFolder = await createTempFolder(); + try { + let iter = 0; + for await (const secret of secrets) { + if (ignoreKeys.includes(secret.secretKey)) { + // eslint-disable-next-line no-continue + continue; + } + + iter += 1; + const secretFilePath = join(tempFolder, `${iter}.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" + }); + } + } + } finally { + await deleteTempFolder(tempFolder); + } +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b6c00985a..f42431195 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", + secretDetectionIgnoreKeys: "The list of secret keys 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 986963e47..af47a6537 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -204,6 +204,18 @@ 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 }[]; + }) + ), + PARAMS_FOLDER_SECRET_DETECTION_ENABLED: zodStrBool.default("false"), + // HSM HSM_LIB_PATH: zpStr(z.string().optional()), HSM_PIN: zpStr(z.string().optional()), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ea4cf9676..01f58494b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1231,6 +1231,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 beef663b9..1564b4bcb 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -264,7 +264,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ auditLogsRetentionDays: true, hasDeleteProtection: true, secretSharing: true, - showSnapshotsLegacy: true + showSnapshotsLegacy: true, + secretDetectionIgnoreKeys: 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..5992b1a33 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -369,7 +369,8 @@ 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), + secretDetectionIgnoreKeys: z.array(z.string()).optional().describe(PROJECTS.UPDATE.secretDetectionIgnoreKeys) }), response: { 200: z.object({ @@ -392,7 +393,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, + secretDetectionIgnoreKeys: req.body.secretDetectionIgnoreKeys }, 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..a166d40c3 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.secretDetectionIgnoreKeys && !hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ + message: "Only admins can update secret detection ignore keys" + }); + } + 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, + secretDetectionIgnoreKeys: update.secretDetectionIgnoreKeys }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index b8c37a858..82f9a0b8f 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; + secretDetectionIgnoreKeys?: 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..f4f815fbd 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 +522,20 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, secretValue } = inputSecret; + if (secretValue) { + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + secretPath, + [ + { + secretKey: inputSecret.newSecretName || secretName, + secretValue + } + ], + project.secretDetectionIgnoreKeys || [] + ); + } + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId @@ -1585,6 +1615,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(secretPath, inputSecrets, project.secretDetectionIgnoreKeys || []); + // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; @@ -1925,6 +1958,18 @@ export const secretV2BridgeServiceFactory = ({ }); await $validateSecretReferences(projectId, permission, secretReferences, tx); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + secretPath, + secretsToUpdate + .filter((el) => el.secretValue) + .map((el) => ({ + secretKey: el.newSecretName || el.secretKey, + secretValue: el.secretValue as string + })), + project.secretDetectionIgnoreKeys || [] + ); + 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..e4e9d2dd1 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, + secretDetectionIgnoreKeys }) => { 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, + secretDetectionIgnoreKeys } ); return data.workspace; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 7d6f68821..e96b7099e 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; + secretDetectionIgnoreKeys: string[]; }; export type WorkspaceEnv = { @@ -81,6 +82,7 @@ export type UpdateProjectDTO = { newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; + secretDetectionIgnoreKeys?: 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..022951dff 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 { SecretDetectionIgnoreKeysSection } from "../SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection"; 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/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx new file mode 100644 index 000000000..bc3f04b98 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx @@ -0,0 +1,147 @@ +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({ + ignoreKeys: z + .object({ + key: z.string().trim().min(1, "Secret key name is required") + }) + .array() + .default([]) +}); + +type TForm = z.infer; + +export const SecretDetectionIgnoreKeysSection = () => { + const { currentWorkspace } = useWorkspace(); + const { membership } = useProjectPermission(); + const { mutateAsync: updateProject } = useUpdateProject(); + + const { + control, + formState: { isSubmitting, isDirty }, + handleSubmit, + reset + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + ignoreKeys: [] + } + }); + + const ignoreKeysFormFields = useFieldArray({ + control, + name: "ignoreKeys" + }); + + useEffect(() => { + const existingIgnoreKeys = currentWorkspace?.secretDetectionIgnoreKeys || []; + reset({ + ignoreKeys: + existingIgnoreKeys.length > 0 ? existingIgnoreKeys.map((key) => ({ key })) : [{ key: "" }] // Show one empty field by default + }); + }, [currentWorkspace?.secretDetectionIgnoreKeys, reset]); + + const handleIgnoreKeysSubmit = async ({ ignoreKeys }: TForm) => { + try { + await updateProject({ + projectID: currentWorkspace.id, + secretDetectionIgnoreKeys: ignoreKeys.map((item) => item.key) + }); + + createNotification({ + text: "Successfully updated secret detection ignore keys", + type: "success" + }); + } catch { + createNotification({ + text: "Failed updating secret detection ignore keys", + type: "error" + }); + } + }; + + const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + + if (!currentWorkspace) return null; + + return ( +
+
+

Secret Detection Ignore Keys

+
+

+ Define secret keys that should be ignored when scanning parameter folders for misplaced + secrets. These keys will not trigger policy violation alerts even if they contain sensitive + data. +

+ +
+
+

Ignored Secret Keys

+
+ {ignoreKeysFormFields.fields.map(({ id: ignoreKeyFieldId }, i) => ( +
+
+ {i === 0 && Secret Key Name} + ( + + + + )} + /> +
+ ignoreKeysFormFields.remove(i)} + isDisabled={!isAdmin} + > + + +
+ ))} +
+ +
+
+
+ + +
+
+ ); +}; From 7fdee073d832871e275e30e9e9d77bc6ccc7d1e4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 26 Jul 2025 03:16:39 +0800 Subject: [PATCH 2/5] misc: add secret checker in change policy branch --- .../secret-approval-request-service.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 2be0361e0..f0c719802 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"; @@ -1407,6 +1408,19 @@ export const secretApprovalRequestServiceFactory = ({ projectId }); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + secretPath, + [ + ...(data[SecretOperations.Create] || []), + ...(data[SecretOperations.Update] || []).filter((el) => el.secretValue) + ].map((el) => ({ + secretKey: el.secretKey, + secretValue: el.secretValue as string + })), + project.secretDetectionIgnoreKeys || [] + ); + // for created secret approval change const createdSecrets = data[SecretOperations.Create]; if (createdSecrets && createdSecrets?.length) { From 585cb1b30c86d6c01d3b9860080324f2a374d9ec Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 26 Jul 2025 03:26:24 +0800 Subject: [PATCH 3/5] misc: used promise all --- .../secret-scanning-v2-fns.ts | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) 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 917f992d3..99aa5b91f 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 @@ -14,6 +14,7 @@ import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-qu 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"; @@ -184,25 +185,23 @@ export const scanSecretPolicyViolations = async ( const tempFolder = await createTempFolder(); try { - let iter = 0; - for await (const secret of secrets) { - if (ignoreKeys.includes(secret.secretKey)) { - // eslint-disable-next-line no-continue - continue; - } + const scanPromises = secrets + .filter((secret) => !ignoreKeys.includes(secret.secretKey)) + .map(async (secret) => { + const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`); + await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); - iter += 1; - const secretFilePath = join(tempFolder, `${iter}.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" - }); - } - } + 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); } From e430abfc9e78396127b9d22e5205d830b54a12a3 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 29 Jul 2025 04:56:50 +0800 Subject: [PATCH 4/5] misc: addressed comments --- ...821_add-secret-detection-ignore-values.ts} | 8 +-- backend/src/db/schemas/projects.ts | 2 +- .../secret-approval-request-service.ts | 3 +- .../secret-scanning-v2-fns.ts | 13 ++-- backend/src/lib/api-docs/constants.ts | 2 +- backend/src/lib/config/env.ts | 6 +- backend/src/server/routes/sanitizedSchemas.ts | 2 +- .../src/server/routes/v1/project-router.ts | 7 ++- .../src/services/project/project-service.ts | 6 +- backend/src/services/project/project-types.ts | 2 +- .../secret-v2-bridge-service.ts | 11 ++-- frontend/src/hooks/api/workspace/queries.tsx | 4 +- frontend/src/hooks/api/workspace/types.ts | 4 +- .../ProjectGeneralTab/ProjectGeneralTab.tsx | 4 +- .../SecretDetectionIgnoreValuesSection.tsx} | 60 ++++++++++--------- 15 files changed, 72 insertions(+), 62 deletions(-) rename backend/src/db/migrations/{20250725171821_add-secret-detection-ignore-keys.ts => 20250725171821_add-secret-detection-ignore-values.ts} (73%) rename frontend/src/pages/secret-manager/SettingsPage/components/{SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx => SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx} (65%) diff --git a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts similarity index 73% rename from backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts rename to backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts index dfdf5ecfc..c8257b771 100644 --- a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts +++ b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts @@ -3,17 +3,17 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys"))) { + if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues"))) { await knex.schema.alterTable(TableName.Project, (t) => { - t.specificType("secretDetectionIgnoreKeys", "text[]"); + t.specificType("secretDetectionIgnoreValues", "text[]"); }); } } export async function down(knex: Knex): Promise { - if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys")) { + if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues")) { await knex.schema.alterTable(TableName.Project, (t) => { - t.dropColumn("secretDetectionIgnoreKeys"); + t.dropColumn("secretDetectionIgnoreValues"); }); } } diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index fd8b273f2..08dc1eee0 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -31,7 +31,7 @@ export const ProjectsSchema = z.object({ secretSharing: z.boolean().default(true), showSnapshotsLegacy: z.boolean().default(false), defaultProduct: z.string().nullable().optional(), - secretDetectionIgnoreKeys: z.string().array().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 f0c719802..b5331e897 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 @@ -1410,6 +1410,7 @@ export const secretApprovalRequestServiceFactory = ({ const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, [ ...(data[SecretOperations.Create] || []), @@ -1418,7 +1419,7 @@ export const secretApprovalRequestServiceFactory = ({ secretKey: el.secretKey, secretValue: el.secretValue as string })), - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); // for created secret approval change 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 99aa5b91f..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 @@ -165,28 +165,29 @@ export const parseScanErrorMessage = (err: unknown): string => { }; export const scanSecretPolicyViolations = async ( + projectId: string, secretPath: string, secrets: { secretKey: string; secretValue: string }[], - ignoreKeys: string[] + ignoreValues: string[] ) => { const appCfg = getConfig(); if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) { return; } - const paramFolderSecretDetectionPaths = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.map((el) => el.secretPath) ?? []; - const isPathMatched = paramFolderSecretDetectionPaths.some((pattern) => - picomatch.isMatch(secretPath, pattern, { strictSlashes: false }) + + const match = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.find( + (el) => el.projectId === projectId && picomatch.isMatch(secretPath, el.secretPath, { strictSlashes: false }) ); - if (!isPathMatched) { + if (!match) { return; } const tempFolder = await createTempFolder(); try { const scanPromises = secrets - .filter((secret) => !ignoreKeys.includes(secret.secretKey)) + .filter((secret) => !ignoreValues.includes(secret.secretValue)) .map(async (secret) => { const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`); await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f42431195..f3b4cbca0 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -705,7 +705,7 @@ export const PROJECTS = { 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", - secretDetectionIgnoreKeys: "The list of secret keys to ignore for secret detection." + 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 af47a6537..bc32d2b05 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -211,10 +211,9 @@ const envSchema = z .optional() .transform((val) => { if (!val) return undefined; - return JSON.parse(val) as { secretPath: string }[]; + return JSON.parse(val) as { secretPath: string; projectId: string }[]; }) ), - PARAMS_FOLDER_SECRET_DETECTION_ENABLED: zodStrBool.default("false"), // HSM HSM_LIB_PATH: zpStr(z.string().optional()), @@ -353,7 +352,8 @@ const envSchema = z isHsmConfigured: 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(",") + SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","), + PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0 })); export type TEnvConfig = Readonly>; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 1564b4bcb..1a69f3845 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -265,7 +265,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ hasDeleteProtection: true, secretSharing: true, showSnapshotsLegacy: true, - secretDetectionIgnoreKeys: true + secretDetectionIgnoreValues: true }); export const SanitizedTagSchema = SecretTagsSchema.pick({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 5992b1a33..6f981f61f 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -370,7 +370,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 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), - secretDetectionIgnoreKeys: z.array(z.string()).optional().describe(PROJECTS.UPDATE.secretDetectionIgnoreKeys) + secretDetectionIgnoreValues: z + .array(z.string()) + .optional() + .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues) }), response: { 200: z.object({ @@ -394,7 +397,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: req.body.slug, secretSharing: req.body.secretSharing, showSnapshotsLegacy: req.body.showSnapshotsLegacy, - secretDetectionIgnoreKeys: req.body.secretDetectionIgnoreKeys + 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 a166d40c3..153f627fc 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -667,9 +667,9 @@ export const projectServiceFactory = ({ } } - if (update.secretDetectionIgnoreKeys && !hasRole(ProjectMembershipRole.Admin)) { + if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ - message: "Only admins can update secret detection ignore keys" + message: "Only admins can update secret detection ignore values" }); } @@ -683,7 +683,7 @@ export const projectServiceFactory = ({ secretSharing: update.secretSharing, defaultProduct: update.defaultProduct, showSnapshotsLegacy: update.showSnapshotsLegacy, - secretDetectionIgnoreKeys: update.secretDetectionIgnoreKeys + secretDetectionIgnoreValues: update.secretDetectionIgnoreValues }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 82f9a0b8f..02f89bc38 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -96,7 +96,7 @@ export type TUpdateProjectDTO = { slug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; - secretDetectionIgnoreKeys?: string[]; + 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 f4f815fbd..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 @@ -301,6 +301,7 @@ export const secretV2BridgeServiceFactory = ({ const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, [ { @@ -308,7 +309,7 @@ export const secretV2BridgeServiceFactory = ({ secretValue: inputSecret.secretValue } ], - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue); @@ -525,6 +526,7 @@ export const secretV2BridgeServiceFactory = ({ if (secretValue) { const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, [ { @@ -532,7 +534,7 @@ export const secretV2BridgeServiceFactory = ({ secretValue } ], - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); } @@ -1616,7 +1618,7 @@ export const secretV2BridgeServiceFactory = ({ throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); const project = await projectDAL.findById(projectId); - await scanSecretPolicyViolations(secretPath, inputSecrets, project.secretDetectionIgnoreKeys || []); + await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []); // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); @@ -1960,6 +1962,7 @@ export const secretV2BridgeServiceFactory = ({ const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, secretsToUpdate .filter((el) => el.secretValue) @@ -1967,7 +1970,7 @@ export const secretV2BridgeServiceFactory = ({ secretKey: el.newSecretName || el.secretKey, secretValue: el.secretValue as string })), - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); const bulkUpdatedSecrets = await fnSecretBulkUpdate({ diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index e4e9d2dd1..7a0e3d2e2 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -282,7 +282,7 @@ export const useUpdateProject = () => { newSlug, secretSharing, showSnapshotsLegacy, - secretDetectionIgnoreKeys + secretDetectionIgnoreValues }) => { const { data } = await apiRequest.patch<{ workspace: Workspace }>( `/api/v1/workspace/${projectID}`, @@ -292,7 +292,7 @@ export const useUpdateProject = () => { slug: newSlug, secretSharing, showSnapshotsLegacy, - secretDetectionIgnoreKeys + secretDetectionIgnoreValues } ); return data.workspace; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index e96b7099e..33eb0909f 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -40,7 +40,7 @@ export type Workspace = { hasDeleteProtection: boolean; secretSharing: boolean; showSnapshotsLegacy: boolean; - secretDetectionIgnoreKeys: string[]; + secretDetectionIgnoreValues: string[]; }; export type WorkspaceEnv = { @@ -82,7 +82,7 @@ export type UpdateProjectDTO = { newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; - secretDetectionIgnoreKeys?: string[]; + 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 022951dff..a542facf5 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -4,7 +4,7 @@ import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection"; import { EnvironmentSection } from "../EnvironmentSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; -import { SecretDetectionIgnoreKeysSection } from "../SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection"; +import { SecretDetectionIgnoreValuesSection } from "../SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection"; import { SecretSharingSection } from "../SecretSharingSection"; import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection"; import { SecretTagsSection } from "../SecretTagsSection"; @@ -20,7 +20,7 @@ export const SecretSettingsTab = () => { - {config.paramsFolderSecretDetectionEnabled && } + {config.paramsFolderSecretDetectionEnabled && } ); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx similarity index 65% rename from frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx rename to frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index bc3f04b98..07617000f 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -12,9 +12,9 @@ import { useUpdateProject } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; const formSchema = z.object({ - ignoreKeys: z + ignoreValues: z .object({ - key: z.string().trim().min(1, "Secret key name is required") + value: z.string().trim().min(1, "Secret value is required") }) .array() .default([]) @@ -22,7 +22,7 @@ const formSchema = z.object({ type TForm = z.infer; -export const SecretDetectionIgnoreKeysSection = () => { +export const SecretDetectionIgnoreValuesSection = () => { const { currentWorkspace } = useWorkspace(); const { membership } = useProjectPermission(); const { mutateAsync: updateProject } = useUpdateProject(); @@ -35,37 +35,39 @@ export const SecretDetectionIgnoreKeysSection = () => { } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - ignoreKeys: [] + ignoreValues: [] } }); - const ignoreKeysFormFields = useFieldArray({ + const ignoreValuesFormFields = useFieldArray({ control, - name: "ignoreKeys" + name: "ignoreValues" }); useEffect(() => { - const existingIgnoreKeys = currentWorkspace?.secretDetectionIgnoreKeys || []; + const existingIgnoreValues = currentWorkspace?.secretDetectionIgnoreValues || []; reset({ - ignoreKeys: - existingIgnoreKeys.length > 0 ? existingIgnoreKeys.map((key) => ({ key })) : [{ key: "" }] // Show one empty field by default + ignoreValues: + existingIgnoreValues.length > 0 + ? existingIgnoreValues.map((value) => ({ value })) + : [{ value: "" }] // Show one empty field by default }); - }, [currentWorkspace?.secretDetectionIgnoreKeys, reset]); + }, [currentWorkspace?.secretDetectionIgnoreValues, reset]); - const handleIgnoreKeysSubmit = async ({ ignoreKeys }: TForm) => { + const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => { try { await updateProject({ projectID: currentWorkspace.id, - secretDetectionIgnoreKeys: ignoreKeys.map((item) => item.key) + secretDetectionIgnoreValues: ignoreValues.map((item) => item.value) }); createNotification({ - text: "Successfully updated secret detection ignore keys", + text: "Successfully updated secret detection ignore values", type: "success" }); } catch { createNotification({ - text: "Failed updating secret detection ignore keys", + text: "Failed updating secret detection ignore values", type: "error" }); } @@ -78,41 +80,41 @@ export const SecretDetectionIgnoreKeysSection = () => { return (
-

Secret Detection Ignore Keys

+

Secret Detection Ignore Values

- Define secret keys that should be ignored when scanning parameter folders for misplaced - secrets. These keys will not trigger policy violation alerts even if they contain sensitive - data. + Define secret values that should be ignored when scanning parameter folders for misplaced + secrets. These values will not trigger policy violation alerts even if they contain + sensitive data.

-
+
-

Ignored Secret Keys

+

Ignored Secret Values

- {ignoreKeysFormFields.fields.map(({ id: ignoreKeyFieldId }, i) => ( -
+ {ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => ( +
- {i === 0 && Secret Key Name} + {i === 0 && Secret Value} ( - + )} />
ignoreKeysFormFields.remove(i)} + onClick={() => ignoreValuesFormFields.remove(i)} isDisabled={!isAdmin} > @@ -124,10 +126,10 @@ export const SecretDetectionIgnoreKeysSection = () => { leftIcon={} size="xs" variant="outline_bg" - onClick={() => ignoreKeysFormFields.append({ key: "" })} + onClick={() => ignoreValuesFormFields.append({ value: "" })} isDisabled={!isAdmin} > - Add Ignore Key + Add Ignore Value
From 7949142ea7dae7034bf2954e7a12c5a182d0a4d4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 28 Jul 2025 23:32:05 -0400 Subject: [PATCH 5/5] update text for secret params --- .../SecretDetectionIgnoreValuesSection.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index 07617000f..48d0e69e8 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -80,17 +80,12 @@ export const SecretDetectionIgnoreValuesSection = () => { return (
-

Secret Detection Ignore Values

+

Secret Detection

-

- Define secret values that should be ignored when scanning parameter folders for misplaced - secrets. These values will not trigger policy violation alerts even if they contain - sensitive data. -

+

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.

-

Ignored Secret Values

{ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => (
@@ -129,7 +124,7 @@ export const SecretDetectionIgnoreValuesSection = () => { onClick={() => ignoreValuesFormFields.append({ value: "" })} isDisabled={!isAdmin} > - Add Ignore Value + Add value to ignore