Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-docker-non-root.mjs
T

97 lines
3.0 KiB
JavaScript

#!/usr/bin/env node
// Asserts that every runtime stage in the root Dockerfile drops to a non-root USER
// before its HEALTHCHECK/CMD/ENTRYPOINT directive. Fails the gate if any runtime
// stage (web, api, worker) ships as root.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const RUNTIME_STAGES = new Set(["web", "api", "worker"]);
function readDockerfile(rootDir) {
return fs.readFileSync(path.join(rootDir, "Dockerfile"), "utf8");
}
function splitStages(content) {
const stages = [];
const lines = content.split(/\r?\n/);
let current = null;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const fromMatch = /^FROM\b.*\bAS\s+([A-Za-z0-9_-]+)\s*$/i.exec(line);
if (fromMatch) {
if (current) {
current.endLine = i;
stages.push(current);
}
current = { name: fromMatch[1], startLine: i + 1, endLine: lines.length, body: [] };
continue;
}
if (current) {
current.body.push(line);
}
}
if (current) {
stages.push(current);
}
return stages;
}
function findRuntimeViolations(stage) {
const userIndex = stage.body.findIndex((line) => /^USER\s+(\S+)/i.test(line));
const userValue = userIndex >= 0 ? /^USER\s+(\S+)/i.exec(stage.body[userIndex])?.[1] : null;
const cmdIndex = stage.body.findIndex((line) => /^(CMD|ENTRYPOINT)\b/i.test(line));
const violations = [];
if (cmdIndex < 0) {
return violations;
}
if (userIndex < 0 || !userValue) {
violations.push(`stage "${stage.name}" has CMD/ENTRYPOINT but no USER directive — runs as root`);
return violations;
}
if (userValue === "root" || userValue === "0") {
violations.push(`stage "${stage.name}" USER is "${userValue}" — runtime must be non-root`);
}
if (userIndex > cmdIndex) {
violations.push(`stage "${stage.name}" USER must come before CMD/ENTRYPOINT (USER at offset ${userIndex}, CMD at offset ${cmdIndex})`);
}
return violations;
}
function main() {
const rootDir = path.resolve(fileURLToPath(import.meta.url), "..", "..");
const content = readDockerfile(rootDir);
const stages = splitStages(content);
const runtimeStages = stages.filter((stage) => RUNTIME_STAGES.has(stage.name));
const failures = [];
const passes = [];
for (const expected of RUNTIME_STAGES) {
if (!runtimeStages.some((stage) => stage.name === expected)) {
failures.push(`Dockerfile is missing the "${expected}" runtime stage`);
}
}
for (const stage of runtimeStages) {
const violations = findRuntimeViolations(stage);
if (violations.length === 0) {
passes.push(`Dockerfile stage "${stage.name}" drops to non-root before CMD/ENTRYPOINT`);
} else {
failures.push(...violations.map((message) => `Dockerfile: ${message}`));
}
}
for (const line of passes) console.log(`PASS ${line}`);
for (const line of failures) console.error(`FAIL ${line}`);
console.log(`Summary: ${passes.length} passed, 0 observed, ${failures.length} failed`);
if (failures.length > 0) {
process.exit(1);
}
}
main();