116 lines
3.3 KiB
JavaScript
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`);
|
|
}
|