misc: addressed comments

This commit is contained in:
Sheen Capadngan
2025-07-29 04:56:50 +08:00
parent 585cb1b30c
commit e430abfc9e
15 changed files with 72 additions and 62 deletions

View File

@@ -3,17 +3,17 @@ import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}

View File

@@ -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<typeof ProjectsSchema>;

View File

@@ -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

View File

@@ -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}`);

View File

@@ -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."

View File

@@ -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<z.infer<typeof envSchema>>;

View File

@@ -265,7 +265,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
hasDeleteProtection: true,
secretSharing: true,
showSnapshotsLegacy: true,
secretDetectionIgnoreKeys: true
secretDetectionIgnoreValues: true
});
export const SanitizedTagSchema = SecretTagsSchema.pick({

View File

@@ -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,

View File

@@ -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;

View File

@@ -96,7 +96,7 @@ export type TUpdateProjectDTO = {
slug?: string;
secretSharing?: boolean;
showSnapshotsLegacy?: boolean;
secretDetectionIgnoreKeys?: string[];
secretDetectionIgnoreValues?: string[];
};
} & Omit<TProjectPermission, "projectId">;

View File

@@ -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({

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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 = () => {
<SecretSharingSection />
<SecretSnapshotsLegacySection />
<PointInTimeVersionLimitSection />
{config.paramsFolderSecretDetectionEnabled && <SecretDetectionIgnoreKeysSection />}
{config.paramsFolderSecretDetectionEnabled && <SecretDetectionIgnoreValuesSection />}
<BackfillSecretReferenceSecretion />
</div>
);

View File

@@ -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<typeof formSchema>;
export const SecretDetectionIgnoreKeysSection = () => {
export const SecretDetectionIgnoreValuesSection = () => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const { mutateAsync: updateProject } = useUpdateProject();
@@ -35,37 +35,39 @@ export const SecretDetectionIgnoreKeysSection = () => {
} = useForm<TForm>({
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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Secret Detection Ignore Keys</p>
<p className="text-xl font-semibold">Secret Detection Ignore Values</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
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.
</p>
<form onSubmit={handleSubmit(handleIgnoreKeysSubmit)} autoComplete="off">
<form onSubmit={handleSubmit(handleIgnoreValuesSubmit)} autoComplete="off">
<div className="mb-4">
<p className="mb-3 text-sm font-medium text-gray-300">Ignored Secret Keys</p>
<p className="mb-3 text-sm font-medium text-gray-300">Ignored Secret Values</p>
<div className="flex flex-col space-y-2">
{ignoreKeysFormFields.fields.map(({ id: ignoreKeyFieldId }, i) => (
<div key={ignoreKeyFieldId} className="flex items-end space-x-2">
{ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => (
<div key={ignoreValueFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Secret Key Name</span>}
{i === 0 && <span className="text-xs text-mineshaft-400">Secret Value</span>}
<Controller
control={control}
name={`ignoreKeys.${i}.key`}
name={`ignoreValues.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} placeholder="PUBLIC_API_KEY" isDisabled={!isAdmin} />
<Input {...field} placeholder="sk-1234567890abcdef" isDisabled={!isAdmin} />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete ignore key"
ariaLabel="delete ignore value"
className="bottom-0.5 h-9"
variant="outline_bg"
onClick={() => ignoreKeysFormFields.remove(i)}
onClick={() => ignoreValuesFormFields.remove(i)}
isDisabled={!isAdmin}
>
<FontAwesomeIcon icon={faTrash} />
@@ -124,10 +126,10 @@ export const SecretDetectionIgnoreKeysSection = () => {
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => ignoreKeysFormFields.append({ key: "" })}
onClick={() => ignoreValuesFormFields.append({ value: "" })}
isDisabled={!isAdmin}
>
Add Ignore Key
Add Ignore Value
</Button>
</div>
</div>