Files
Portfolio/scripts/release/run-privacy-workers.mjs

116 lines
3.3 KiB
JavaScript

#!/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`);
}