81 lines
3.0 KiB
JavaScript
81 lines
3.0 KiB
JavaScript
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;
|
|
}
|