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