feat(trivy): add reusable Trivy vulnerability scan workflow - #92
Conversation
Scans the built distribution tree of an app (or core) for known HIGH/CRITICAL vulnerabilities and fails the job when any are found. Two details are load-bearing: - scan-type must be rootfs, not fs. The fs scanner does not enable Trivy's gobinary analyzer, so bundled Go binaries go unscanned - which is exactly how a Go 1.22.4 rclone with 23 HIGH/CRITICAL CVEs shipped unnoticed in migrate_to_ocis. - It scans the unpacked tree, not the .tar.gz: Trivy does not recurse into archives, and composer/npm dependencies only exist after the build. The scan path is autodetected because the app repos disagree on the layout: 29 use build/dist/<app>, 14 the older build/artifacts/appstore/<app>, and notes uses build/appstore/<app>. Callers with none of those pass scan-path. A missing tree fails the job rather than scanning nothing, since Trivy reports a nonexistent path as clean. Build command is taken as a make target rather than a shell string so a caller cannot inject arbitrary commands into the runner. Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Validated on real CIRan this workflow from a throwaway caller in So: the build ran, Blocker before this can be rolled out: the org actions allowlistThe first attempt failed at startup (0s, no jobs, Isolated by pointing the identical caller at owncloud/admin#232 adds the v0.36.0 SHA (commit is from 2026-04-22, so the 7-day cooldown gate passes). That has to merge before this one, otherwise every caller startup-fails. The alternative is pinning v0.35.0 here instead, which needs no allowlist change but contradicts the pin-to-latest-release convention. |
Green half validated tooowncloud/migrate_to_ocis#58 ran the same workflow on top of the upstream rclone v1.75.0 swap, with → job passes, exit 0. So all four behaviours this workflow exists for are now demonstrated on real CI, not reasoned about:
Both throwaway PRs and branches are being deleted now. |
The cache was keyed on build/yarn.lock, which only core generates.
setup-node hard-fails when the lockfile it is pointed at is missing
("Dependencies lock file is not found in ..."), so the two app repos that
need node — external and files_mediaviewer, whose Makefiles $(error) at
parse time without npm/yarn — would never reach the build step.
Caching is now opt-in: node-cache-dependency-path empty means no cache,
and core passes build/yarn.lock explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
|
Pushed one fix found while pre-checking the repos that have no
Pre-check results for the four odd repos, for the record:
Signing is skipped in all of them without a key ( |
Every other reusable workflow here is self-invoked by test.yml, so a regression is caught before it reaches the callers. The trivy workflow was the exception, which is the worst place for that gap: its failure mode is a scan that reports clean because it never looked at anything. Scanning this repository is not an option - `make dist` produces only a tarball, and trivy does not look inside archives - so the gate needs a real app. The new app-repository input, matching the one php-codestyle and php-unit already have, lets the local invocation build and scan one. Two jobs, one per dist layout, so both branches of the scan path autodetection stay covered; the second also exercises the optional PHP and Node.js setup steps. Also quote the candidate list in the error path, reported by shellcheck. Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
|
Added the missing self-test. What was added
Verified locally: both
Arguably the gate already earned its keep: without it this dependency would have surfaced in 44 app repos instead of in one PR. |
|
owncloud/admin#232 is merged and the org sync applied it, so CI starts now instead of
Nothing to change here — the workflow is unmodified. I'll re-run this PR's CI once #234 lands. Worth noting for the rollout: had the self-test not existed, this would have landed as 44 app repos plus core all failing in "Set up job" with an error that names an action nobody wrote down. |
|
reopening to retrigger CI now that owncloud/admin#234 is synced |
|
owncloud/admin#234 is synced and both gate jobs are green (run 32145827918). The evidence that they scanned something rather than passing on an empty path:
Both report summaries are empty, so exit 0. Step-level: Both dist layouts are therefore covered, and |
| candidates="${SCAN_PATH}" | ||
| else | ||
| candidates="build/dist/${APP_NAME} | ||
| build/artifacts/appstore/${APP_NAME} |
There was a problem hiding this comment.
It took me a minute to realise that this and the net line are part of a string, not commands inside the else/fi.
Why
migrate_to_ocisshipped a bundled rclone built with Go 1.22.4 carrying 23 HIGH/CRITICAL CVEs — includingCVE-2025-68121(crypto/tls),CVE-2024-45337(x/crypto) andCVE-2026-33186(gRPC) — and nothing anywhere reported it. No app repo has any vulnerability scan today, and the one scan that did exist (on the docker image) was blind to it.This adds the scan the app repos are missing. Callers: one
security-scan.ymlper repo, rolled out across the 43 oc11 app repos plusowncloud/core.What it does
checkout → optional PHP/node setup →
make <target>→trivy rootfsover the built tree, failing on HIGH/CRITICAL with a fix available.Three decisions are load-bearing, each learned the hard way:
1.
scan-type: rootfs, notfs. Thefsscanner does not enable Trivy's gobinary analyzer, so bundled Go binaries are silently skipped. Verified on trivy 0.55.0 and 0.69.3 against the same tree:An
fsscan here would have reproduced the exact blind spot this is meant to close.2. The unpacked tree, not the tarball. Trivy does not recurse into archives, and composer/npm dependencies only exist after a build.
3.
trivyignoresdefaults to empty. The action fails hard on a named ignore file that does not exist:Defaulting it to
.trivyignorewould have broken all 43 repos on day one, since none has the file. Trivy reads a repo-root.trivyignoreon its own, so a repo opts in simply by committing one — verified: 23 findings → 21 after listing two CVEs.Scan path autodetection
The app repos disagree on where
make distwrites, so the path is detected rather than configured — that keeps the caller file byte-identical in every repo:build/dist/<app>build/artifacts/appstore/<app>build/appstore/<app>notesAnything else passes
scan-path. A missing tree fails the job rather than scanning nothing — Trivy reports a nonexistent path as clean, so a broken build would otherwise read as a green scan.Inputs
app-namemake-targetdistdist-dirphp-version''php-extensionscurl, gd, json, xml, zipphp-versionis setnode-version''dist-dirrunsyarn run clean-modulesscan-pathseverityHIGH,CRITICALignore-unfixedtrueskip-files''trivyignores''The build command is taken as a make target, not a shell string, so a caller cannot inject arbitrary commands into the runner.
Testing
shacross six cases: all three layouts, explicitscan-path, missing tree, and explicit-but-wrong path. The two failure cases exit 1 and print thebuild/tree.releases/latest+ tag dereference:trivy-actionv0.36.0,setup-php2.37.2,setup-nodev7.0.0.yamllintprofile matches the existingbuild.yml.owncloud/server:11.0.0the analyzer never ran (0 findings, CI green); with the exec bit as the only change it reports all 23; with upstream rclone v1.75.0 it runs and reports clean.Related
🤖 Generated with Claude Code