feat: isolate privacy scanning and release signing
This commit is contained in:
115
scripts/release/run-privacy-workers.mjs
Normal file
115
scripts/release/run-privacy-workers.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const safeImage = /^[A-Za-z0-9._/-]+@sha256:[a-f0-9]{64}$/u;
|
||||
const safePath = /^\/[A-Za-z0-9._/-]+$/u;
|
||||
|
||||
export function buildWorkerCommand({
|
||||
runtime,
|
||||
image,
|
||||
inputPath,
|
||||
outputPath,
|
||||
worker,
|
||||
}) {
|
||||
if (runtime !== "docker" && runtime !== "podman")
|
||||
throw new Error("Only Docker or Podman isolation runtimes are supported.");
|
||||
if (!safeImage.test(image))
|
||||
throw new Error(
|
||||
"Worker image must be pinned by a lowercase SHA-256 digest.",
|
||||
);
|
||||
if (
|
||||
!safePath.test(inputPath) ||
|
||||
!safePath.test(outputPath) ||
|
||||
inputPath.includes("..") ||
|
||||
outputPath.includes("..")
|
||||
)
|
||||
throw new Error("Worker mounts must be explicit absolute safe paths.");
|
||||
if (
|
||||
worker !== "worker-a" &&
|
||||
worker !== "worker-b" &&
|
||||
worker !== "matcher" &&
|
||||
worker !== "coordinator"
|
||||
)
|
||||
throw new Error("Unknown isolated workload.");
|
||||
return [
|
||||
runtime,
|
||||
"run",
|
||||
"--rm",
|
||||
"--pull=never",
|
||||
"--network=none",
|
||||
"--read-only",
|
||||
"--cap-drop=ALL",
|
||||
"--security-opt=no-new-privileges",
|
||||
"--user=65532:65532",
|
||||
"--memory=256m",
|
||||
"--memory-swap=256m",
|
||||
"--cpus=1",
|
||||
"--pids-limit=64",
|
||||
"--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=64m",
|
||||
`--mount=type=bind,src=${inputPath},dst=/input,readonly`,
|
||||
`--mount=type=bind,src=${outputPath},dst=/output`,
|
||||
image,
|
||||
];
|
||||
}
|
||||
|
||||
export function validateScanObjectDescriptor(value) {
|
||||
if (
|
||||
value === null ||
|
||||
typeof value !== "object" ||
|
||||
!/^[a-f0-9]{64}$/u.test(value.inputDigest) ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(value.checkId) ||
|
||||
typeof value.mediaType !== "string"
|
||||
)
|
||||
throw new Error("Scan object descriptor is invalid.");
|
||||
if (value.encrypted === true)
|
||||
throw new Error("Encrypted containers are unsupported and fail closed.");
|
||||
if (value.malformed === true)
|
||||
throw new Error("Malformed containers fail closed.");
|
||||
if (
|
||||
!Number.isSafeInteger(value.archiveDepth) ||
|
||||
value.archiveDepth < 0 ||
|
||||
value.archiveDepth > 3
|
||||
)
|
||||
throw new Error("Nested archive depth exceeds the limit.");
|
||||
if (
|
||||
!Number.isSafeInteger(value.expandedBytes) ||
|
||||
value.expandedBytes < 0 ||
|
||||
value.expandedBytes > 64 * 1024 * 1024
|
||||
)
|
||||
throw new Error("Expanded archive size exceeds the limit.");
|
||||
if (!Array.isArray(value.members))
|
||||
throw new Error("Container member inventory is required.");
|
||||
for (const member of value.members) {
|
||||
if (
|
||||
member === null ||
|
||||
typeof member !== "object" ||
|
||||
typeof member.path !== "string" ||
|
||||
member.path.startsWith("/") ||
|
||||
member.path.includes("\\") ||
|
||||
member.path.split("/").some((part) => part === ".." || part === ".") ||
|
||||
member.path !== member.path.normalize("NFC") ||
|
||||
!["file", "directory"].includes(member.entryType)
|
||||
) {
|
||||
throw new Error(
|
||||
"Archive traversal, symlink, hard-link, device, or non-canonical member rejected.",
|
||||
);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function liveContainerStatus() {
|
||||
return {
|
||||
schema: "portfolio-live-container-validation/v1",
|
||||
status: "pending",
|
||||
reason:
|
||||
"Requires locally cached, provenance-verified pinned images; network pulls are prohibited.",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
) {
|
||||
process.stdout.write(`${JSON.stringify(liveContainerStatus())}\n`);
|
||||
}
|
||||
Reference in New Issue
Block a user