Repository navigation
ci: move image vulnerability scanning out of CI to a weekly scan - #1014
Merged
Merged
Conversation
Remove the Trivy step from the ci.yml image job; the build, boot smoke test and verified export still gate publish. Add image-scan.yml, a weekly and on-demand scan of the published image on both architectures that uploads SARIF and keeps one issue per vulnerability, closing it once cleared.
Pin Trivy v0.75.0 (setup-trivy defaults to latest), scan each architecture by its own manifest digest, fail the commit picker on anything but "not found", close only issues the workflow's bot filed, escape backslashes and @mentions in issue text, and drop the last "CI scanned" claim from the Dockerfile.
Owner
Author
Review record — Claude Opus 5.5 (local agent, read-only), 3 roundsReviewed at the owner's request. CodeRabbit was not triggered. Round 1 —
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A blocking Trivy scan stalled every PR and every publish on one base-image CVE that no PR could fix (#1006, CVE-2026-84782). Keycloak, Harbor and Kyverno scan on a schedule, report to the Security tab and file issues; none block PRs on an image scan. Owner decision: no scan in CI, no ignore file, report instead.
Closes #1006
Scope
ci.ymlimagejob: Trivy step removed and the job renamedImage build + smoke test (<arch>). The two-architecture matrix, build, lock-drift guard, boot smoke test and verified export are untouched and still gatepublish; nocontinue-on-error.image-scan.yml(new, weekly Mon 07:00 UTC plusworkflow_dispatch): scans the published:sha-<commit>of the newest of main's last 30 commits that has one, onlinux/amd64andlinux/arm64, each by its own manifest digest resolved from the index. Trivy is pinned (setup-trivyversion: v0.75.0; Dependabot does not bump that input, so bump it by hand, recorded in Deploy: container/image hardening — non-root USER, pinned base digests, image vulnerability scan #267). SARIF goes to the Security tab withcategory: image-scan-<arch>. Three jobs, each with its own permissions:resolveandscanread only (scanaddssecurity-events: write), and onlyissuesholdsissues: write, so the third-party Trivy action never runs beside it..github/scripts/image-scan-issues.mjs(+ test, run in the classifier self-test job): one issue per vulnerability id (open or closed), matched by the id in the scanner's own title ([HIGH] CVE-… in <pkgs> (container image)), never by body. New issues getdependenciesanddocker, no priority label. A found id gets a comment; a cleared id gets a comment naming the commit and both digests, then is closed. It closes only issues in the scanner's title shape carrying both labels and authored bygithub-actions(a human-filed issue is never closed; updates stay title-only), and only when both architecture reports parse and name the image; otherwise it exits 1 before touching any issue. Theissuesjob also needs both scan jobs to succeed.git grep -ni trivy:AGENTS.md(Deploy: container/image hardening — non-root USER, pinned base digests, image vulnerability scan #267, CI: skip the image and web jobs on pull requests that change no code #782 paragraph, CI builds the runtime image but never publishes it — deploy-by-digest has no artifact to point at #351publish.needs, Publish multi-arch images so Cluckwork runs on arm64 (Raspberry Pi and ARM VPS) #995), Deploy: container/image hardening — non-root USER, pinned base digests, image vulnerability scan #267 record (reversal, reason, accepted costs), CI: dependency-vulnerability and SAST gates #146, Runtime image lacks libgssapi_krb5.so.2 — Npgsql logs a load error on every connection #332, CI builds the runtime image but never publishes it — deploy-by-digest has no artifact to point at #351, CI: skip the image and web jobs on pull requests that change no code #782, Publish multi-arch images so Cluckwork runs on arm64 (Raspberry Pi and ARM VPS) #995 records,docs/releasing.md,SECURITY.md,deploy/README.md. The CI: dependency-vulnerability and SAST gates #146 NuGet and npm gates are untouched. Timings in the CI: measure where Build and test's 10 minutes go, and what the backend suite covers #775 and CI: skip the image and web jobs on pull requests that change no code #782 records name the old check; they are dated measurements and are left as written.dependabot.ymlandchanged-paths.mjschanged by comment text only: each named the Trivy gate as a behaviour. No logic changed; the #782 classifier tests still pass (25/25 with the new ones).Tradeoffs
workflow_dispatchonimage-scan.ymlscans on demand (dry_runavailable); not a required release step.dockerbumps; the next scan closes the issue.Blast Radius
CI and docs only.
publishand promotion are unchanged apart from no longer waiting on a scan.image-scan.ymlcannot block anything. Left unmerged, every code PR and everymainpush stays red or unpublished while the CVE stands.Verification
Dry-run against the live repo, using real Trivy reports of the published
sha-ad258d8fimage (both architectures). #1006 is not matched, and nothing was created, edited or closed:Reports are of the published
sha-ad258d8fimage, scanned by child digest with Trivy 0.75.0. (b) to (h) feed the decision logic fixture issues (--issues, bot-authored unless noted); the tests cover each case and each was proven red by reverting its logic (7 mutations); the unit tests cover the same plus "human issue never closed" and "id in another issue's text not matched". Mutation checks: dropping the label, author or architecture check, or escaping pipes before backslashes, each turned a test red.actionlintshows only the 4 existing infos inrelease-please.yml; the two ImagePin tests andSchemaDocsTests/SolutionTestProjectSplitTestspass.The scan step itself ran as the pinned Trivy 0.75.0 container locally (same flags). Its first run in Actions is untested: after merge, dispatch it once with
gh workflow run image-scan.yml --ref main -f dry_run=trueand check that (1)resolvepicks a commit, (2) bothscanlegs go green with a table in the summary and a SARIF category each under Security > Code scanning, and (3) theissuessummary says it would create an issue for CVE-2026-84782. Then dispatch withdry_run=falseto file it, and check that the issue it creates is authored bygithub-actions. The bot's author login is inferred, not observed:ghreportsGITHUB_TOKEN-filed issues asapp/github-actionsand the code normalises that, but no issue in this repo has ever been filed by it. If the author differs, closing silently never happens.This PR's own CI should show the
imagejob green on both architectures with no scan.Review round 1 (
a4bd642d)Pinned Trivy; per-architecture manifest digests (
RepoDigestswas the index digest for both); author check for closing; honest close comment ("no longer reported as a fixable HIGH or CRITICAL"); picker fails on anything but not-found; backslash and@escaping with tests;persist-credentials: falseon the issues checkout; Dockerfile comment reworded.Round 2:
resolvepins the index digest once and both scan legs take their child manifest from it; a test now asserts the advisory autolink (breaking the link logic turns it red).Ignore by closing (owner rule)
Closing a scanner issue ignores that CVE; reopening un-ignores it. Matching is by the CVE id in the scanner's title shape, open or closed, never by body, so closed #1006 (no CVE in its title) does not match.
ignoredfirst if it carries the label.ignoredonce with one comment in the same step (the comment, then the label); a run that finds the label already there posts nothing, and removing the label never posts. A CVE that cleared (label removed) and later returns is re-labelled with a new comment.ignoredif present); removeignoredfrom a closed issue, since it means "still present".ignoredlabel is created idempotently by the issues job (gh label create --force); issue listing now paginates overGET /issues?state=all.Accepted costs (recorded in #267): an ignore has no expiry, and a CVE that returns after being fixed (its issue closed by the scan) is not reopened and gets no new issue; it is re-labelled with a comment, which is the only notification.
The comment posted when
ignoredis added (rendered from the real reports):Tests: (e2) label plus one comment, second run no comment; (e3) already labelled, no comment; (g2) label removed on clear, CVE returns, label and comment again; no comment on unlabel; and a live-run test with a stub
ghshowing the orderlabel create,issue comment,issue edit, then nothing on the second run. Each was proven red by reverting its logic.