Pin dependency policy exceptions to versions

This commit is contained in:
2026-08-27 12:51:53 +01:00
parent f28b9ee87d
commit 8098495abf
10 changed files with 125 additions and 27 deletions
+37 -5
View File
@@ -19,8 +19,9 @@ export async function loadPolicy(file) {
if (!KEYS.has(key)) throw new Error(`unknown policy key: ${key}`);
}
const policy = { ...structuredClone(DEFAULT_POLICY), ...parsed };
validateStringArray(policy, "allowInstallScripts");
validateStringArray(policy, "allowDirectLifecycleScripts");
validateVersionExceptions(policy.allowInstallScripts, "allowInstallScripts");
validateVersionExceptions(policy.allowDirectLifecycleScripts, "allowDirectLifecycleScripts");
validateArchiveExceptions(policy.archiveSizeExceptions);
validateSourceExceptions(policy.sourceEvidenceExceptions);
validateMap(policy, "trustedRepositories", false);
validateMap(policy, "trustedMaintainers", true);
@@ -60,9 +61,40 @@ function validateSourceExceptions(value) {
}
}
function validateStringArray(policy, key) {
if (!Array.isArray(policy[key]) || policy[key].some((value) => typeof value !== "string" || !value)) {
throw new Error(`${key} must be an array of non-empty strings`);
function validateVersionExceptions(value, key) {
if (!value || Array.isArray(value) || typeof value !== "object") throw new Error(`${key} must be an object`);
for (const [name, exception] of Object.entries(value)) {
validateExceptionBase(name, exception, key, ["versions"]);
if (!Array.isArray(exception.versions) || exception.versions.length === 0 || exception.versions.some((version) => typeof version !== "string" || !version)) {
throw new Error(`${key}.${name}.versions must be an array of non-empty strings`);
}
}
}
function validateArchiveExceptions(value) {
if (!value || Array.isArray(value) || typeof value !== "object") throw new Error("archiveSizeExceptions must be an object");
for (const [name, exception] of Object.entries(value)) {
validateExceptionBase(name, exception, "archiveSizeExceptions", ["version", "maxTarballSizeBytes", "maxUnpackedSizeBytes"]);
if (typeof exception.version !== "string" || !exception.version) throw new Error(`archiveSizeExceptions.${name}.version must be a non-empty string`);
const limits = [exception.maxTarballSizeBytes, exception.maxUnpackedSizeBytes].filter((limit) => limit !== undefined);
if (limits.length === 0 || limits.some((limit) => !Number.isFinite(limit) || limit < 0)) {
throw new Error(`archiveSizeExceptions.${name} must contain at least one non-negative size limit`);
}
}
}
function validateExceptionBase(name, exception, key, allowedFields) {
if (!name || !exception || Array.isArray(exception) || typeof exception !== "object") {
throw new Error(`invalid ${key} entry for ${name || "<empty>"}`);
}
if (Object.keys(exception).some((field) => ![...allowedFields, "reason", "expiresAt"].includes(field))) {
throw new Error(`unknown ${key} field for ${name}`);
}
if (typeof exception.reason !== "string" || exception.reason.trim().length < 20) {
throw new Error(`${key}.${name}.reason must contain at least 20 characters`);
}
if (typeof exception.expiresAt !== "string" || !Number.isFinite(Date.parse(exception.expiresAt))) {
throw new Error(`${key}.${name}.expiresAt must be an ISO date`);
}
}