Files
HealthLog/.github/workflows/trivy.yml
T
Marc-André Bombeck d48bbff01d ci: take the dependency audit off pull requests
`pnpm audit --prod` answers a question about the published advisory database,
not about the diff. The same lockfile passes on Monday and fails on Tuesday
because someone else published, and the branch that turns red changed nothing:
seventeen of the forty-one Security & Quality failures in the last thirty days
were advisory publications on branches that touched no dependency
(GHSA-jqff-g426-hqxp, GHSA-fph4-wmhf-6fwf, GHSA-f65p-4m7j-42xc and
GHSA-5jgf-p345-68v8 in one run alone). A gate that fails for a reason the author
cannot act on is a gate people re-run instead of read.

It keeps running on every push to `main` and once a day on a new schedule
trigger, which is the cadence the question actually has. The other three jobs
sit out the scheduled run. Nothing about what ships is loosened: the container
scan still builds and scans the image on every push to `main`, and the daily
Trivy workflow still scans the lockfile with dev dependencies included and the
published image.

Gate — the trigger set, read back from the parsed workflow:

  triggers: push, pull_request, schedule
  quality          | if: github.event_name != 'schedule'
  dependency-audit | if: github.event_name != 'pull_request'
  secret-scan      | if: github.event_name != 'schedule'
  docker-security  | if: github.event_name == 'push' && github.ref == 'refs/heads/main'
2026-09-09 22:42:54 +02:00

165 lines
6.9 KiB
YAML

name: Trivy Scheduled Scan
# Two scans that the per-change gates cannot do, on a daily cadence.
#
# What already exists and is NOT repeated here:
# - `security.yml` → `dependency-audit` runs `pnpm audit --prod` on every
# push to `main` and once a day, blocking on HIGH/CRITICAL.
# - `security.yml` → `docker-security` builds the image on a push to `main`
# and runs `trivy image` against it, blocking on HIGH/CRITICAL.
#
# Both are change-triggered, and both answer "is this change safe". Neither
# answers "is what we shipped last week still safe", because an advisory
# published today lands against an image nobody is about to rebuild. That is
# what the schedule is for. There is deliberately no push or pull_request
# trigger: adding one would run the same scan the `docker-security` job
# already ran, minutes later, on the same tree.
on:
schedule:
# Daily, 05:17 UTC.
- cron: "17 5 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: trivy-scheduled
cancel-in-progress: true
jobs:
filesystem:
name: Lockfile (including dev dependencies)
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# `pnpm audit --prod` is the blocking gate and it is scoped to
# production dependencies on purpose: a dev-only advisory should not
# stop a release. The gap that scoping leaves is that "dev dependency"
# and "not in the image" are not the same statement.
#
# `pdfjs-dist` is the worked example. It sits in `devDependencies`,
# and `src/lib/documents/rasterize-pdf.ts` imports it at runtime, so
# Next's standalone output tracing copies it into the production
# image. `pnpm audit --prod` cannot see it. This scan can: without
# the flag Trivy reads 627 packages out of `pnpm-lock.yaml`, with it
# 1455, and `pdfjs-dist` is only in the second set.
#
# Trivy's own `--help` lists the flag as supported for npm, yarn and
# gradle. It works on `pnpm-lock.yaml` too; the help text is behind
# the parser. Verified against this repository before this workflow
# was written, and worth re-checking if the package count ever
# collapses back to the production figure.
#
# Report-only, and that is a deliberate choice rather than a soft
# one. The findings this widening surfaces are overwhelmingly
# transitive dev-tool advisories with fixes available upstream;
# failing the run on them from day one produces a red badge that gets
# muted within a week, and a muted signal is worse than none. The
# SARIF upload puts every finding in the repository's code-scanning
# alerts, which persist, notify, and can be triaged one at a time.
# The blocking answer to "does this ship" stays where it is, in
# `security.yml`.
- name: Trivy filesystem scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
# No action input maps to `--include-dev-deps`; Trivy reads every
# flag from a `TRIVY_`-prefixed environment variable, so this is
# the documented way to reach it through the action.
TRIVY_INCLUDE_DEV_DEPS: "true"
with:
scan-type: fs
scan-ref: .
scanners: vuln
format: sarif
output: trivy-filesystem.sarif
severity: "CRITICAL,HIGH,MEDIUM"
limit-severities-for-sarif: true
exit-code: "0"
# Generated Prisma client. Nobody edits it, the fix for anything
# found in it is `pnpm prisma generate`, and it is ~9 MB.
skip-dirs: src/generated
- name: Upload results
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: trivy-filesystem.sarif
category: trivy-filesystem
published-image:
name: Published image
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: read
security-events: write
steps:
# The `docker-security` job scans an image it just built from the
# current `main`. This one scans the image operators actually pull,
# which is the one built from the last release tag and unchanged
# since. The two answer different questions and the gap between them
# widens every day a release stays out.
#
# Trivy resolves the multi-arch manifest to the runner's platform, so
# this covers `linux/amd64`. The arm64 manifest shares every
# application layer and differs only in native binaries.
- name: Log in to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# `.trivyignore` lives at the repository root and Trivy reads it
# automatically, so the suppressions the blocking gate already
# carries apply here too and the two scans cannot disagree about
# what is accepted.
- name: Checkout for .trivyignore
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Fails on CRITICAL only. A critical advisory against the image
# self-hosters are running right now is worth a red badge and a
# notification; HIGH goes to code scanning with everything else,
# because the release pipeline already refuses to publish an image
# carrying one and anything that appears here appeared after the
# fact.
- name: Trivy published-image scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: image
image-ref: ghcr.io/mbombeck/healthlog:latest
scanners: vuln
format: sarif
output: trivy-published-image.sarif
severity: "CRITICAL,HIGH,MEDIUM"
limit-severities-for-sarif: true
exit-code: "0"
- name: Upload results
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: trivy-published-image.sarif
category: trivy-published-image
# Separate table pass so the SARIF upload above always runs, even
# when this one fails the job. A scan whose failure suppresses its
# own report is a scan you have to re-run by hand to read.
- name: Fail on a critical finding in the published image
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: image
image-ref: ghcr.io/mbombeck/healthlog:latest
scanners: vuln
format: table
severity: "CRITICAL"
exit-code: "1"