const DIGEST_PIN = /^[^\s@]+:[^\s@]+@sha256:[a-f0-9]{64}$/; const EXACT_PYTHON_PIN = /^[A-Za-z0-9_.-]+(?:\[[A-Za-z0-9_,.-]+\])?==[^*<>=!~\s;]+(?:\s*;.+)?$/; export function checkContainerPolicy(dockerfile) { const violations = []; for (const [index, line] of dockerfile.split(/\r?\n/).entries()) { const match = line.match(/^\s*FROM\s+(\S+)/i); if (!match || match[1].toLowerCase() === 'scratch') continue; if (!DIGEST_PIN.test(match[1])) { violations.push({ code: 'unpinned-image', line: index + 1, value: match[1] }); } } return violations; } /** * Every `npm ci` / `npm install` in the image must carry `--ignore-scripts`. * * Why this check exists * --------------------- * The builder stage had `--ignore-scripts` from the start; the runtime stage — * the one that actually ships — did not, and it installs `build-essential` on * the same layer. A hijacked dependency's install script would have run there * with a compiler in reach: the most permissive spot in the pipeline. The * design and the PR both claimed install scripts were "off everywhere", so the * gap was invisible in the prose (found reviewing PR #1074). * * Only RUN instructions are inspected, and comment lines are dropped first: * this Dockerfile explains the Playwright bin collision in prose that mentions * `npm ci` several times, and matching those would make the check cry wolf. * Line continuations are joined, because the runtime install is one link in a * long `&&` chain spread over many lines. */ export function checkDockerfileInstallPolicy(dockerfile) { const violations = []; const lines = dockerfile.split(/\r?\n/); let buffer = ''; let startLine = 0; let inRun = false; for (const [index, raw] of lines.entries()) { const line = raw.replace(/\s+#.*$/, ''); if (/^\s*#/.test(raw)) continue; if (!inRun) { if (!/^\s*RUN\s/i.test(line)) continue; inRun = true; startLine = index + 1; buffer = ''; } buffer += line.replace(/\\\s*$/, ' '); if (/\\\s*$/.test(line)) continue; for (const command of buffer.split(/&&|;|\|\|/)) { if (!/\bnpm\b[^|]*\s(?:ci|install|i)(?:\s|$)/.test(command)) continue; if (/--ignore-scripts\b/.test(command)) continue; violations.push({ code: 'npm-install-runs-scripts', line: startLine, value: command.trim(), }); } buffer = ''; inRun = false; } return violations; } export function checkPythonRequirements(requirements) { const violations = []; for (const [index, rawLine] of requirements.split(/\r?\n/).entries()) { const value = rawLine.replace(/\s+#.*$/, '').trim(); if (!value || value.startsWith('#')) continue; if (value.startsWith('-') || value.includes(' @ ') || /(?:https?|git\+|file):/i.test(value)) { violations.push({ code: 'non-index-python-dependency', line: index + 1, value }); continue; } if (!EXACT_PYTHON_PIN.test(value)) { violations.push({ code: 'unpinned-python-dependency', line: index + 1, value }); } } return violations; }