Files
Claude a609cf86c7 Load Bifrost providers and failover from config.json
Bifrost v2.3.0 loads $APP_DIR/config.json (APP_DIR=/app/data) at startup
and never read the mounted compose/bifrost.yaml, so no providers or
failover routes were configured. Replace it with compose/bifrost.config.json
(validated against the transports/v2.3.0 config.schema.json), bind-mounted
read-only at /app/data/config.json over the writable bifrost-data volume:

- providers opencode-go (host :11434), openrouter, vllm (host :8000), keys
  as env.BIFROST_API_KEY / env.OPENROUTER_API_KEY references
- one global routing rule targeting opencode-go with fallbacks
  openrouter -> vllm

session_affinity and the Retry-After clamp have no config.json equivalent
in v2.3.0 (the schema rejects unknown top-level keys) and are dropped.
The verifier now applies its generic rules to compose/*.json; tests, the
wave-1 gate, README, .env.example and agentgateway.yaml point at the new file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012AuVk4nEvBXaTjs4G6wdW4
2026-10-16 19:58:18 +00:00

703 lines
26 KiB
TypeScript

/**
* scripts/verify-stack.ts — Deterministic static contract verifier (Wave 1).
*
* Audits every in-scope file and FAILS CLOSED (exit 1, exact `file:line` +
* rule name) when any security/pinning invariant is broken:
*
* NO_FLOATING_TAGS — `image:` ending in `:latest` or lacking a `:tag`.
* NO_DOCKER_SOCK — any volume/mount referencing docker.sock/podman.sock.
* LOOPBACK_PORTS_ONLY — any published `ports:` entry not starting `127.0.0.1:`.
* HARDENED_CAPS — any compose service missing `cap_drop: [ALL]` or
* `no-new-privileges:true`.
* SELINUX_VOLUME_LABELS — any bind mount (`./…` or `/…` host path) in compose
* or devcontainer files missing `:z`, `:Z` or `:ro,Z`.
* NO_AGPL_IN_DEV_OR_GAME — any image referencing firecrawl,
* RFingAdam/mcp-blender, bytebot, 0xflux/ghidra-mcp
* or pentagi (case-insensitive).
* ENV_PLACEHOLDER_LEAK — a real `.env` containing a `CHANGEME_` token.
* (`.env.example` is the template and MUST keep them.)
* SELINUX_CONFINED — a quadlet that opts out of SELinux confinement
* (`SecurityLabelType=spc_t`/`spc`, `SecurityLabelDisable=true`)
* or runs privileged.
* QUADLET_SOCKET_ACTIVATION — a quadlet container that can start on its own
* (`[Install]`/`WantedBy=`, no `StopWhenUnneeded=true`) or
* lacks a loopback `.socket` + systemd-socket-proxyd
* `.service` pair that Requires=/After= it and exits idle.
*
* Local builds: a compose service with `build:` must tag its image as
* `localhost/<name>:<version>` (NO_FLOATING_TAGS); Dockerfile `FROM` and
* `COPY --from=` images must carry an explicit version tag.
*
* Scanned roots (each skipped gracefully when absent):
* docker-compose.yml, compose/*.yml (+ compose/*.yaml, *.json generic rules),
* devcontainers/** (.json + Dockerfile), images/** /Dockerfile,
* quadlets/*.container, quadlets/*.socket, quadlets/*.service,
* .env (placeholder check only).
*
* Usage: bun run scripts/verify-stack.ts [--root <dir>]
* --root defaults to the repository root; tests point it at fixture trees.
*
* Zero dependencies beyond the Bun/Node standard library.
*/
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { basename, join, relative, resolve } from "node:path";
import { dockerfileImages, isLocalImage, isPinned, parseImageRef } from "./lib/image-ref";
interface Violation {
file: string;
line: number;
rule: string;
message: string;
}
function parseRoot(argv: string[]): string {
let root = join(import.meta.dir, "..");
for (let i = 0; i < argv.length; i++) {
if (argv[i] === "--root") {
const value = argv[i + 1];
if (!value) {
console.error("verify-stack: --root requires a directory");
process.exit(2);
}
root = resolve(value);
i += 1;
} else {
console.error(`verify-stack: unknown argument: ${argv[i]}`);
process.exit(2);
}
}
if (!existsSync(root) || !statSync(root).isDirectory()) {
console.error(`verify-stack: --root is not a directory: ${root}`);
process.exit(2);
}
return root;
}
const REPO_ROOT = parseRoot(process.argv.slice(2));
const violations: Violation[] = [];
let filesScanned = 0;
function repoRel(path: string): string {
return relative(REPO_ROOT, path) || path;
}
function fail(file: string, line: number, rule: string, message: string): void {
violations.push({ file, line, rule, message });
}
function readLines(path: string): string[] {
return readFileSync(path, "utf8").split("\n");
}
function indentOf(line: string): number {
const match = /^( *)/.exec(line);
return match ? match[1].length : 0;
}
function stripComment(line: string): string {
// Cut trailing `# comment`, but keep `#` inside quotes (image digests never
// contain `#`, URLs may contain fragments — only cut ` #` outside quotes).
let inSingle = false;
let inDouble = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === "'" && !inDouble) inSingle = !inSingle;
else if (ch === '"' && !inSingle) inDouble = !inDouble;
else if (ch === "#" && !inSingle && !inDouble && (i === 0 || line[i - 1] === " " || line[i - 1] === "\t")) {
return line.slice(0, i);
}
}
return line;
}
function unquote(value: string): string {
const t = value.trim().replace(/,+$/, "").trim();
if (t.length >= 2 && ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")))) {
return t.slice(1, -1);
}
return t;
}
// ---------------------------------------------------------------------------
// Compose-file checks (docker-compose.yml + compose/*.yml files that declare
// `services:`). Non-service configs (bifrost.config.json, agentgateway.yaml)
// still get the generic substring rules via checkGenericContent.
// ---------------------------------------------------------------------------
const BANNED_IMAGE_TOKENS = ["firecrawl", "rfingadam", "bytebot", "0xflux", "pentagi"];
function checkGenericContent(path: string, lines: string[]): void {
const display = repoRel(path);
lines.forEach((raw, idx) => {
const lineNo = idx + 1;
const lowered = raw.toLowerCase();
if (lowered.includes("docker.sock") || lowered.includes("podman.sock")) {
fail(display, lineNo, "NO_DOCKER_SOCK", `socket mount reference forbidden: ${raw.trim()}`);
}
const imageMatch = /image\s*:\s*(.+)/.exec(stripComment(raw));
if (imageMatch) {
const image = unquote(imageMatch[1]);
if (BANNED_IMAGE_TOKENS.some((token) => image.toLowerCase().includes(token))) {
fail(display, lineNo, "NO_AGPL_IN_DEV_OR_GAME", `banned AGPL/root-escape image: ${image}`);
}
} else if (
BANNED_IMAGE_TOKENS.some((token) => lowered.includes(token)) &&
/image|from\s|container/i.test(raw)
) {
fail(display, lineNo, "NO_AGPL_IN_DEV_OR_GAME", `banned token in image context: ${raw.trim()}`);
}
});
}
interface ServiceBlock {
name: string;
startLine: number;
lines: Array<{ text: string; lineNo: number }>;
}
function splitServices(lines: string[]): ServiceBlock[] {
const servicesIdx = lines.findIndex((l) => /^services\s*:\s*(#.*)?$/.test(l));
if (servicesIdx === -1) return [];
const blocks: ServiceBlock[] = [];
let current: ServiceBlock | null = null;
let inServices = true;
for (let i = servicesIdx + 1; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === "" || stripComment(line).trim() === "") continue;
// A new top-level section (e.g. `volumes:`, `networks:`) ends `services:`.
// Its nested keys (e.g. ` nats-data:`) are NOT services — stop parsing.
if (/^[A-Za-z0-9_.-]+\s*:\s*(#.*)?$/.test(line) && indentOf(line) === 0) {
inServices = false;
current = null;
continue;
}
if (!inServices) continue;
const serviceHead = /^ ([A-Za-z0-9_.-]+)\s*:\s*(#.*)?$/.exec(line);
if (serviceHead && indentOf(line) === 2) {
current = { name: serviceHead[1], startLine: i + 1, lines: [] };
blocks.push(current);
continue;
}
if (/^\S/.test(line)) {
// Back to top level — services section ended.
current = null;
inServices = false;
continue;
}
if (current) current.lines.push({ text: line, lineNo: i + 1 });
}
return blocks;
}
function checkFloatingTags(display: string, lines: string[]): void {
lines.forEach((raw, idx) => {
const match = /image\s*:\s*(.+)/.exec(stripComment(raw));
if (!match) return;
const image = unquote(match[1]).trim();
if (image === "") {
fail(display, idx + 1, "NO_FLOATING_TAGS", "empty image reference");
return;
}
// Registry ports (ghcr.io:443/…) contain a colon before the first slash;
// the tag is whatever follows the LAST colon after the last slash.
const lastSlash = image.lastIndexOf("/");
const tagPart = image.slice(lastSlash + 1);
if (!tagPart.includes(":")) {
fail(display, idx + 1, "NO_FLOATING_TAGS", `image missing explicit version tag: ${image}`);
return;
}
const tag = tagPart.slice(tagPart.lastIndexOf(":") + 1);
if (tag === "latest" || tag === "") {
fail(display, idx + 1, "NO_FLOATING_TAGS", `floating/empty image tag forbidden: ${image}`);
}
});
}
function checkLoopbackPorts(display: string, block: ServiceBlock): void {
let inPorts = false;
let portsIndent = -1;
for (const { text, lineNo } of block.lines) {
const code = stripComment(text);
if (!inPorts) {
const keyMatch = /^(\s*)ports\s*:\s*(#.*)?$/.exec(code);
if (keyMatch) {
inPorts = true;
portsIndent = indentOf(code);
}
continue;
}
if (code.trim() === "") continue;
if (indentOf(code) <= portsIndent) {
inPorts = false;
continue;
}
const entry = /^\s*-\s*(.+?)\s*$/.exec(code);
if (!entry) continue; // long-syntax keys handled below; short entries only here
const value = unquote(entry[1].trim());
// Long-syntax mapping continuation lines (`target:`, `host_ip:`) are not
// list entries with a dash — but a dash entry could still be long syntax
// start (`- target: 8080`). Require explicit host_ip loopback then.
if (/^(target|host_ip|published|protocol|mode)\s*:/.test(value)) {
continue;
}
if (!value.startsWith("127.0.0.1:")) {
fail(
display,
lineNo,
"LOOPBACK_PORTS_ONLY",
`service "${block.name}" port must bind 127.0.0.1:<host>:<container>, got: ${value}`,
);
}
}
// Long-syntax host_ip check: any host_ip that is not 127.0.0.1 fails.
for (const { text, lineNo } of block.lines) {
const m = /host_ip\s*:\s*(.+)/.exec(stripComment(text));
if (!m) continue;
const ip = unquote(m[1]).trim();
if (ip !== "127.0.0.1") {
fail(display, lineNo, "LOOPBACK_PORTS_ONLY", `service "${block.name}" host_ip must be 127.0.0.1, got: ${ip}`);
}
}
}
function checkHardenedCaps(display: string, block: ServiceBlock): void {
const codeLines = block.lines.map((l) => stripComment(l.text));
const body = codeLines.join("\n");
// Accept both inline (`cap_drop: [ALL]`) and block-list (`cap_drop:\n - ALL`) forms.
const inlineDrop = /cap_drop\s*:\s*\[[^\]]*ALL[^\]]*\]/.test(body);
let blockDrop = false;
for (let i = 0; i < codeLines.length; i++) {
if (/^\s*cap_drop\s*:\s*$/.test(codeLines[i])) {
const keyIndent = indentOf(codeLines[i]);
for (let j = i + 1; j < codeLines.length; j++) {
if (codeLines[j].trim() === "") continue;
if (indentOf(codeLines[j]) <= keyIndent) break;
if (/^\s*-\s*ALL\s*$/.test(codeLines[j])) blockDrop = true;
}
}
}
if (!inlineDrop && !blockDrop) {
fail(display, block.startLine, "HARDENED_CAPS", `service "${block.name}" missing cap_drop: [ALL]`);
}
if (!/no-new-privileges\s*:?\s*true/.test(body)) {
fail(
display,
block.startLine,
"HARDENED_CAPS",
`service "${block.name}" missing security_opt no-new-privileges:true`,
);
}
}
function checkSelinuxLabels(display: string, block: ServiceBlock): void {
let inVolumes = false;
let volumesIndent = -1;
for (const { text, lineNo } of block.lines) {
const code = stripComment(text);
if (!inVolumes) {
const keyMatch = /^(\s*)volumes\s*:\s*(#.*)?$/.exec(code);
if (keyMatch) {
inVolumes = true;
volumesIndent = indentOf(code);
}
continue;
}
if (code.trim() === "") continue;
if (indentOf(code) <= volumesIndent) {
inVolumes = false;
continue;
}
const entry = /^\s*-\s*(.+?)\s*$/.exec(code);
if (!entry) continue;
const value = unquote(entry[1].trim());
if (value.startsWith("type:") || value.startsWith("source:") || value.startsWith("target:")) continue;
const hostPart = value.split(":")[0].trim();
const isBind = hostPart.startsWith("./") || hostPart.startsWith("../") || hostPart.startsWith("/") || hostPart === "." || hostPart === "..";
if (!isBind) continue; // named volume (e.g. nats-data:/data) — no label needed
if (!/:ro,[zZ]\b/.test(value) && !/:[zZ]\b/.test(value)) {
fail(
display,
lineNo,
"SELINUX_VOLUME_LABELS",
`service "${block.name}" bind mount missing :z/:Z/:ro,Z SELinux label: ${value}`,
);
}
}
}
function checkLocalBuild(display: string, block: ServiceBlock): void {
const build = block.lines.find((l) => /^ {4}build\s*:/.test(stripComment(l.text)));
if (!build) return;
const imageLine = block.lines.find((l) => /^ {4}image\s*:/.test(stripComment(l.text)));
if (!imageLine) {
fail(display, block.startLine, "NO_FLOATING_TAGS", `service "${block.name}" builds an image without a versioned image: tag`);
return;
}
const image = unquote(stripComment(imageLine.text).replace(/^\s*image\s*:/, ""));
let local = false;
try {
const ref = parseImageRef(image);
local = isLocalImage(ref) && isPinned(ref) && ref.digest === null;
} catch {
local = false;
}
if (!local) {
fail(
display,
imageLine.lineNo,
"NO_FLOATING_TAGS",
`service "${block.name}" builds locally; its image must be localhost/<name>:<version>, got: ${image}`,
);
}
}
function checkComposeFile(path: string): void {
const display = repoRel(path);
const lines = readLines(path);
filesScanned += 1;
checkGenericContent(path, lines);
checkFloatingTags(display, lines);
const services = splitServices(lines);
for (const block of services) {
checkLoopbackPorts(display, block);
checkHardenedCaps(display, block);
checkSelinuxLabels(display, block);
checkLocalBuild(display, block);
}
}
function checkDockerfile(path: string): void {
const display = repoRel(path);
filesScanned += 1;
checkGenericContent(path, readLines(path));
for (const found of dockerfileImages(path, REPO_ROOT)) {
let pinned = false;
try {
pinned = isPinned(parseImageRef(found.ref));
} catch {
pinned = false;
}
if (!pinned) {
fail(display, found.line, "NO_FLOATING_TAGS", `Dockerfile base image not pinned to a version: ${found.ref}`);
}
}
}
// ---------------------------------------------------------------------------
// Devcontainer / quadlet / env checks
// ---------------------------------------------------------------------------
function checkDevcontainerFile(path: string): void {
const display = repoRel(path);
filesScanned += 1;
let raw: string;
try {
raw = readFileSync(path, "utf8");
} catch {
fail(display, 1, "NO_DOCKER_SOCK", "unreadable devcontainer file (fail closed)");
return;
}
const lines = raw.split("\n");
checkGenericContent(path, lines);
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
fail(display, 1, "NO_DOCKER_SOCK", "devcontainer JSON does not parse (fail closed)");
return;
}
const text = JSON.stringify(parsed);
const lowered = text.toLowerCase();
if (lowered.includes("docker.sock") || lowered.includes("podman.sock")) {
fail(display, 1, "NO_DOCKER_SOCK", "devcontainer mounts a container socket");
}
// Bind-mount SELinux labels: inspect mount strings in runArgs/mounts.
const mountStrings: string[] = [];
const collect = (node: unknown): void => {
if (typeof node === "string") {
if (node.includes(":/") || node.startsWith("./") || node.startsWith("/")) mountStrings.push(node);
return;
}
if (Array.isArray(node)) {
for (const item of node) collect(item);
return;
}
if (node !== null && typeof node === "object") {
for (const value of Object.values(node as Record<string, unknown>)) collect(value);
}
};
collect(parsed);
mountStrings.forEach((mount) => {
const hostPart = mount.split(":")[0];
const isBind =
hostPart.startsWith("./") || hostPart.startsWith("../") || hostPart.startsWith("/") || mount.includes("type=bind");
if (isBind && !/:ro,[zZ]\b/.test(mount) && !/:[zZ]\b/.test(mount) && !/,Z\b/.test(mount) && !/,z\b/.test(mount)) {
fail(display, 1, "SELINUX_VOLUME_LABELS", `devcontainer bind mount missing SELinux label: ${mount}`);
}
});
// Pinned-image check for devcontainer `image` fields.
const checkImage = (node: unknown): void => {
if (Array.isArray(node)) {
for (const item of node) checkImage(item);
return;
}
if (node !== null && typeof node === "object") {
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
if (key === "image" && typeof value === "string") {
const image = value.trim();
const tagPart = image.slice(image.lastIndexOf("/") + 1);
if (!tagPart.includes(":") || tagPart.endsWith(":latest")) {
fail(display, 1, "NO_FLOATING_TAGS", `devcontainer image not pinned: ${image}`);
}
} else {
checkImage(value);
}
}
}
};
checkImage(parsed);
}
interface UnitEntry {
section: string;
key: string;
value: string;
lineNo: number;
}
function parseUnit(path: string): UnitEntry[] {
const entries: UnitEntry[] = [];
let section = "";
readLines(path).forEach((raw, idx) => {
const line = raw.trim();
if (line === "" || line.startsWith("#") || line.startsWith(";")) return;
const head = /^\[([^\]]+)\]$/.exec(line);
if (head) {
section = head[1];
return;
}
const kv = /^([A-Za-z0-9]+)\s*=\s*(.*)$/.exec(line);
if (kv) entries.push({ section, key: kv[1], value: kv[2].trim(), lineNo: idx + 1 });
});
return entries;
}
function unitValues(entries: UnitEntry[], key: string): string[] {
return entries
.filter((e) => e.key === key)
.flatMap((e) => e.value.split(/\s+/))
.filter((v) => v !== "");
}
function checkUnitGeneric(path: string): void {
const display = repoRel(path);
readLines(path).forEach((raw, idx) => {
const lowered = raw.toLowerCase();
if (lowered.includes("docker.sock") || lowered.includes("podman.sock")) {
fail(display, idx + 1, "NO_DOCKER_SOCK", `socket reference forbidden: ${raw.trim()}`);
}
});
}
function checkQuadletFile(path: string, sockets: string[], services: string[]): void {
const display = repoRel(path);
filesScanned += 1;
checkUnitGeneric(path);
const entries = parseUnit(path);
const unitName = `${basename(path, ".container")}.service`;
for (const e of entries) {
if (e.key === "Image") {
const image = e.value;
if (!/\.(build|image)$/.test(image)) {
let pinned = false;
try {
pinned = isPinned(parseImageRef(image));
} catch {
pinned = false;
}
if (!pinned) fail(display, e.lineNo, "NO_FLOATING_TAGS", `quadlet image not pinned: ${image}`);
}
if (BANNED_IMAGE_TOKENS.some((token) => image.toLowerCase().includes(token))) {
fail(display, e.lineNo, "NO_AGPL_IN_DEV_OR_GAME", `banned AGPL/root-escape image: ${image}`);
}
}
if (e.key === "PublishPort" && !e.value.startsWith("127.0.0.1:")) {
fail(display, e.lineNo, "LOOPBACK_PORTS_ONLY", `quadlet PublishPort must bind 127.0.0.1, got: ${e.value}`);
}
if (e.key === "Volume") {
const host = e.value.split(":")[0];
const isBind = host.startsWith("/") || host.startsWith("./") || host.startsWith("%");
if (isBind && !/:([^:]*,)?[zZ](,|$)/.test(e.value)) {
fail(display, e.lineNo, "SELINUX_VOLUME_LABELS", `quadlet bind mount missing :z/:Z label: ${e.value}`);
}
}
if (e.key === "SecurityLabelType" && /^spc(_t)?$/.test(e.value)) {
fail(display, e.lineNo, "SELINUX_CONFINED", `SecurityLabelType=${e.value} disables SELinux confinement`);
}
if ((e.key === "SecurityLabelDisable" || e.key === "Privileged") && /^(true|yes|1)$/i.test(e.value)) {
fail(display, e.lineNo, "SELINUX_CONFINED", `${e.key}=${e.value} disables container confinement`);
}
if (e.key === "PodmanArgs" && /--privileged|label=disable|label=type:spc_t/.test(e.value)) {
fail(display, e.lineNo, "SELINUX_CONFINED", `PodmanArgs opts out of confinement: ${e.value}`);
}
if (e.section === "Install") {
fail(
display,
e.lineNo,
"QUADLET_SOCKET_ACTIVATION",
`[Install] ${e.key}= starts the container on its own; only its .socket may be enabled`,
);
}
}
if (!unitValues(entries, "DropCapability").includes("ALL")) {
fail(display, 1, "HARDENED_CAPS", "quadlet missing DropCapability=ALL");
}
if (!entries.some((e) => e.key === "NoNewPrivileges" && /^(true|yes|1)$/i.test(e.value))) {
fail(display, 1, "HARDENED_CAPS", "quadlet missing NoNewPrivileges=true");
}
if (!entries.some((e) => e.section === "Unit" && e.key === "StopWhenUnneeded" && /^(true|yes|1)$/i.test(e.value))) {
fail(display, 1, "QUADLET_SOCKET_ACTIVATION", "quadlet missing [Unit] StopWhenUnneeded=true");
}
// At least one proxy .service (paired with a .socket of the same name) must
// Requires= and After= this container unit.
const pulledBy = services.filter((svc) => {
const svcEntries = parseUnit(svc);
return (
unitValues(svcEntries, "Requires").includes(unitName) &&
unitValues(svcEntries, "After").includes(unitName) &&
sockets.some((sock) => basename(sock, ".socket") === basename(svc, ".service"))
);
});
if (pulledBy.length === 0) {
fail(
display,
1,
"QUADLET_SOCKET_ACTIVATION",
`no quadlets/*.socket + proxy .service pair Requires=/After= ${unitName} (container is never socket-activated)`,
);
}
}
function checkSocketFile(path: string, services: string[]): void {
const display = repoRel(path);
filesScanned += 1;
checkUnitGeneric(path);
const entries = parseUnit(path);
const listens = entries.filter((e) => e.section === "Socket" && /^Listen(Stream|Datagram)$/.test(e.key));
if (listens.length === 0) {
fail(display, 1, "QUADLET_SOCKET_ACTIVATION", "socket unit has no ListenStream=");
}
for (const e of listens) {
if (!e.value.startsWith("127.0.0.1:")) {
fail(display, e.lineNo, "LOOPBACK_PORTS_ONLY", `socket must listen on 127.0.0.1:<port>, got: ${e.value}`);
}
}
const twin = `${basename(path, ".socket")}.service`;
if (!services.some((svc) => basename(svc) === twin)) {
fail(display, 1, "QUADLET_SOCKET_ACTIVATION", `socket has no matching ${twin} proxy unit`);
}
}
function checkProxyServiceFile(path: string): void {
const display = repoRel(path);
filesScanned += 1;
checkUnitGeneric(path);
const entries = parseUnit(path);
const exec = entries.find((e) => e.section === "Service" && e.key === "ExecStart");
if (!exec || !/systemd-socket-proxyd\s/.test(exec.value)) {
fail(display, exec?.lineNo ?? 1, "QUADLET_SOCKET_ACTIVATION", "proxy unit must ExecStart systemd-socket-proxyd");
return;
}
if (!/--exit-idle-time=\S+/.test(exec.value)) {
fail(display, exec.lineNo, "QUADLET_SOCKET_ACTIVATION", "systemd-socket-proxyd needs --exit-idle-time= so the container can stop when idle");
}
const target = exec.value.trim().split(/\s+/).pop() ?? "";
if (!target.startsWith("127.0.0.1:")) {
fail(display, exec.lineNo, "LOOPBACK_PORTS_ONLY", `proxy must forward to 127.0.0.1:<port>, got: ${target}`);
}
}
function checkRealEnv(path: string): void {
const display = repoRel(path);
if (!existsSync(path)) return;
filesScanned += 1;
readLines(path).forEach((raw, idx) => {
if (raw.includes("CHANGEME_")) {
fail(display, idx + 1, "ENV_PLACEHOLDER_LEAK", "real .env still contains a CHANGEME_ placeholder token");
}
});
}
function collectFiles(dir: string, suffix: string, out: string[]): void {
if (!existsSync(dir) || !statSync(dir).isDirectory()) return;
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
collectFiles(full, suffix, out);
} else if (entry.endsWith(suffix)) {
out.push(full);
}
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main(): void {
const composeFiles: string[] = [];
const rootCompose = join(REPO_ROOT, "docker-compose.yml");
if (existsSync(rootCompose)) composeFiles.push(rootCompose);
collectFiles(join(REPO_ROOT, "compose"), ".yml", composeFiles);
// Non-service configs (agentgateway.yaml, bifrost.config.json) get generic
// substring rules only — they declare no services/images/ports.
const genericYaml: string[] = [];
collectFiles(join(REPO_ROOT, "compose"), ".yaml", genericYaml);
collectFiles(join(REPO_ROOT, "compose"), ".json", genericYaml);
const devcontainers: string[] = [];
collectFiles(join(REPO_ROOT, "devcontainers"), ".devcontainer.json", devcontainers);
// Also catch plain .json devcontainer fragments if the tree uses them.
const devJson: string[] = [];
collectFiles(join(REPO_ROOT, "devcontainers"), ".json", devJson);
for (const f of devJson) {
if (!devcontainers.includes(f)) devcontainers.push(f);
}
const quadlets: string[] = [];
collectFiles(join(REPO_ROOT, "quadlets"), ".container", quadlets);
const sockets: string[] = [];
collectFiles(join(REPO_ROOT, "quadlets"), ".socket", sockets);
const proxyServices: string[] = [];
collectFiles(join(REPO_ROOT, "quadlets"), ".service", proxyServices);
const dockerfiles: string[] = [];
collectFiles(join(REPO_ROOT, "images"), "Dockerfile", dockerfiles);
collectFiles(join(REPO_ROOT, "devcontainers"), "Dockerfile", dockerfiles);
for (const file of composeFiles.sort()) checkComposeFile(file);
for (const file of genericYaml.sort()) {
filesScanned += 1;
checkGenericContent(file, readLines(file));
}
for (const file of devcontainers.sort()) checkDevcontainerFile(file);
for (const file of dockerfiles.sort()) checkDockerfile(file);
for (const file of quadlets.sort()) checkQuadletFile(file, sockets, proxyServices);
for (const file of sockets.sort()) checkSocketFile(file, proxyServices);
for (const file of proxyServices.sort()) checkProxyServiceFile(file);
checkRealEnv(join(REPO_ROOT, ".env"));
if (violations.length > 0) {
for (const v of violations) {
console.error(`FAIL [${v.rule}] ${v.file}:${v.line}: ${v.message}`);
}
console.error(`verify-stack: ${violations.length} violation(s) in ${filesScanned} file(s) — FAIL CLOSED`);
process.exit(1);
}
console.log(`PASS: all security contracts hold across ${filesScanned} scanned file(s).`);
}
main();