Skip to content

Add Trivy vulnerability scanning for Docker images - #181

Merged
vharseko merged 3 commits into
OpenIdentityPlatform:masterfrom
vharseko:features/docker-trivy-scan
Oct 22, 2026
Merged

vharseko merged 3 commits into
OpenIdentityPlatform:masterfrom
vharseko:features/docker-trivy-scan

Conversation

@vharseko

@vharseko vharseko commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Adds Docker image vulnerability scanning with Trivy, the same setup as OpenIdentityPlatform/OpenDJ#854 in its final, reviewed form, plus what the later sibling PRs settled (OpenIdentityPlatform/OpenICF#135, OpenIdentityPlatform/OpenIDM#216): a separately cached Trivy install and a scan that cannot fail the build.

build.yml — build-docker now scans the freshly built image (resolved from the local Docker daemon, so the runner's linux/amd64 manifest only) right after the docker test. The image is built from this commit's OpenIG-*.war (the ubuntu-latest-11 artifact; the Dockerfile downloads the release war only when none was copied in), so the scan covers this commit's dependencies plus the tomcat base image. The SARIF report is uploaded via codeql-action/upload-sarif and the full list lives in the Security tab. Only fixable CRITICAL/HIGH CVEs are reported (ignore-unfixed: true plus limit-severities-for-sarif: true — without the latter the severity filter is silently dropped for SARIF output), and only the vulnerability scanner runs (scanners: vuln).

  • The scan never fails the build. Findings exit 0 (no exit-code), and the Install and Scan steps are continue-on-error, so a Trivy or trivy-db registry outage cannot turn build-docker red — a red Build would also skip the snapshot deploy in deploy.yml. The scan step has timeout-minutes: 15: trivy's own --timeout does not bound the Java DB download. The upload runs on !cancelled() and only when a report exists.
  • Caching. The action's built-in DB cache is disabled (cache: false): it saves the ~1 GB trivy DBs per branch scope and day, which would fill the repo's 10 GB actions-cache quota within days and LRU-evict the m2-repository caches (350–480 MiB each) the build-maven matrix depends on. The cache stood at 10.12 GiB when this PR was opened. cache: false would also skip setup-trivy's binary cache and leave an anonymous github.com release lookup in every run (it lost a scan in OpenICF#135), so Trivy v0.70.0 (trivy-action v0.36.0's default) is installed by its own aquasecurity/setup-trivy step with cache: true, and trivy-action runs with skip-setup-trivy: true.
  • The PR check. GitHub creates one Trivy check-run per SARIF tool, next to CodeQL; trivy-build-default is only the SARIF category. That check is diff-scoped: image-path alerts never fall inside a PR diff, so it reports "No new alerts in code changed by this pull request" whatever the scan found. The findings for a PR are in the Security tab under pr:<N> tool:Trivy.
  • The job gets contents: read + security-events: write; SARIF upload also works for fork PRs on public repositories. actions/checkout@v7 (with persist-credentials: false) is added as the first step of the job, as in OpenDJ, so upload-sarif has a git checkout to work from (before download-artifact, since checkout cleans the workspace).

docker-scan.yml (new) — weekly cron (30 5 * * 1, no overlap with CodeQL's 27 3 * * 1) + workflow_dispatch scan of the published openidentityplatform/openig:latest image: new CVEs surface in the already-released image (mostly via the tomcat base image) without any change in this repository. Unlike the build-time scan, unfixed CVEs are reported too — surfacing them in a released image is the point. Report category trivy-image-latest, distinct from trivy-build-default. The scheduled run is skipped in forks; manual runs are always allowed. OpenIG publishes a single image variant, so the matrix has one tag.

  • Only the linux/amd64 manifest is scanned (no --platform is passed); the other published platforms (arm64, ppc64le, s390x) are not.
  • No continue-on-error here: a failed weekly scan is the signal. The job has timeout-minutes: 30, for the same reason as the build-time step.
  • Once it has run, trivy-image-latest exists only on master, so every PR's Trivy check concludes neutral with "1 configuration not found" (as on OpenAM PRs today). This is documented in the workflow header.
  • Its triggers need the file on the default branch, so it cannot run on this PR. I will start it by hand (gh workflow run docker-scan.yml) right after merge and report the run here.

aquasecurity/trivy-action (ed142fd…, v0.36.0 — the current latest) and aquasecurity/setup-trivy (81e5143…, v0.3.1) are pinned by commit SHA; actions/* and github/codeql-action stay on major tags, the policy of #170. .github/dependabot.yml (new, second commit) enables the github-actions ecosystem so the pinned SHAs keep getting updated: weekly, one grouped PR for all actions, labels ci + dependencies. It is byte-identical to the file added in #170, so whichever PR lands first brings it in without a conflict. Not used: TRIVY_INSECURE (global TLS off, and not needed — the image is read from the daemon, the registry is never contacted).

Overlap with #170: both touch build.yml, in different hunks (#170: workflow-level permissions and the uses: pins; here: the build-docker job header, its first step and its tail). The added license header is the identical block, so it should merge cleanly either way.

This PR's own build-docker run exercises the new build-time steps.

@vharseko vharseko added security Security fixes and CVE / vulnerability updates ci Build, CI/CD, and GitHub Actions changes docker Docker image and container changes labels Sep 18, 2026
@vharseko vharseko added the dependencies Pull requests that update a dependency file label Sep 18, 2026
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@maximthomas maximthomas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: The scan is wired the way the description says, and the details that usually go wrong are right.

  • limit-severities-for-sarif: true (build.yml:133, docker-scan.yml:50) keeps the CRITICAL,HIGH filter in effect for SARIF output.
  • aquasecurity/trivy-action is pinned by SHA (ed142fd…, v0.36.0), and .github/dependabot.yml keeps that pin moving.
  • The fork-PR claim holds: this PR's own build-docker run (fork, token SecurityEvents: read) logged "Successfully uploaded results" for trivy-build-default.

question (non-blocking): Should a Trivy outage (DB download, setup-trivy fetch) fail build-docker and the Build workflow?

.github/workflows/build.yml:123-136

exit-code is unset, so findings exit 0. But with cache: false every run downloads trivy-db (114 MiB) and trivy-java-db (923 MiB) from mirror.gcr.io. If both registries in trivy's fallback fail, or setup-trivy cannot fetch the binary, trivy exits non-zero, and the step has no continue-on-error. build-docker then goes red with no code change. On a push to master, deploy.yml:14 (workflow_run of Build, conclusion == 'success') also skips the snapshot deploy for that commit. This is rare (trivy falls back from mirror.gcr.io to ghcr.io on 408/429/5xx) and a re-run fixes it, so it is minor as written. If "findings do not fail the build" was meant as "the scan never fails the build", it is a bug worth fixing here. If failing on an outage is intended, one line in the step comment saying so is enough.

      - name: Scan image for vulnerabilities (Trivy)
        # trivy resolves the image from the local Docker daemon, so only the runner's
        # linux/amd64 manifest is scanned; cache: false keeps the ~1GB trivy DBs from
        # evicting the m2-repository caches out of the repo's 10GB actions-cache quota
        # a scanner outage must not fail the build either; the upload step skips a missing report
        continue-on-error: true
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0

suggestion (non-blocking): docker-scan.yml is never executed before merge.

.github/workflows/docker-scan.yml:19-22, :44-59

The workflow triggers only on schedule and workflow_dispatch, and workflow_dispatch returns 404 until the file is on the default branch, so this PR's checks have no Docker Scan run. The green build-docker run covers only the build.yml copy, which uses a literal filename and a local-registry image. It does not exercise hashFiles(format('trivy-{0}.sarif', matrix.tag)), the remote Docker Hub pull or the job-level fork guard. A typo in any of them would only show up on the first Monday after merge. As written, they look right.

on:
  schedule:
    - cron: '30 5 * * 1'
  workflow_dispatch:
  pull_request:
    paths:
      - '.github/workflows/docker-scan.yml'

Pin: with this trigger, this PR's checks include a Docker Scan run that uploads a trivy-image-latest analysis, and a broken format() guard shows up as a skipped upload step. Or: run gh workflow run docker-scan.yml once, right after merge.


suggestion (non-blocking): The docker-scan.yml scan job has no timeout-minutes.

.github/workflows/docker-scan.yml:28-38

Trivy's default 5-minute --timeout bounds the vuln DB download and the image pull. It does not bound the Java DB update (trivy v0.70.0 javadb/client.go:49 uses context.Background()) or the setup-trivy binary download. A stalled fetch therefore holds the weekly run until GitHub's 360-minute default. In this PR's run, build-docker's scan took about 35 s including both DB downloads.

  scan:
    # Do not run the scheduled scan in forks; manual runs are always allowed.
    if: github.event_name == 'workflow_dispatch' || github.repository == 'OpenIdentityPlatform/OpenIG'
    runs-on: ubuntu-latest
    timeout-minutes: 30

suggestion (non-blocking): The upload steps use always() where their comment asks only for "even if a preceding step failed".

.github/workflows/build.yml:140, .github/workflows/docker-scan.yml:56

Under build.yml's concurrency block (cancel-in-progress: true), always() keeps the upload running on a run that was superseded after its SARIF was written. Nothing wrong is uploaded, because the newer run's analysis arrives minutes later and wins. The gain is a condition that matches the comment and a slightly faster cancel.

        if: ${{ !cancelled() && hashFiles('trivy-results.sarif') != '' }}

suggestion (non-blocking): The new checkouts keep the default persist-credentials, though neither job runs a git command.

.github/workflows/build.yml:77, .github/workflows/docker-scan.yml:40

Nothing leaks today: checkout v7 keeps the token under $RUNNER_TEMP, not in .git/config, and the docker/* and trivy actions get github.token through their own inputs anyway. In a job with security-events: write, persist-credentials: false is free hardening, and zizmor-style linters ask for it.

      - uses: actions/checkout@v7
        with:
          persist-credentials: false

suggestion (non-blocking): The new code leaves actions/checkout@v7 and github/codeql-action/upload-sarif@v4 on mutable tags, while trivy-action is SHA-pinned.

.github/workflows/docker-scan.yml:40, :54, .github/workflows/build.yml:77, :138

#170 pins the uses: lines of build.yml, codeql.yml, deploy.yml and release.yml as they stand on master. Whichever of #170 and #181 lands second therefore leaves these four lines as the only mutable tags in jobs with security-events: write. Exposure does not change today, since codeql.yml already runs the same tags with the same permission. It is a follow-up for whichever PR lands second to name.


note (non-blocking): The description doesn't match the checks.

  • The description says PRs get a "Code scanning results / trivy-build-default" check. This PR's check list shows it as Trivy; trivy-build-default is only the SARIF category.

build.yml: scan the freshly built image in build-docker right after the
docker tests and upload the SARIF report to code scanning (category
trivy-build-default). Only fixable CRITICAL/HIGH CVEs are reported and
findings do not fail the build.

docker-scan.yml (new): weekly scan of the published
openidentityplatform/openig:latest image, unfixed CVEs included, so that
new CVEs in the released image surface without a change in this repo.

Same setup as OpenIdentityPlatform/OpenDJ#854: severity filter kept for
SARIF via limit-severities-for-sarif, trivy DB cache disabled to keep the
~1GB DBs from evicting the m2-repository caches, action pinned by SHA.
- build.yml: continue-on-error on the Install and Scan steps, so a Trivy or
  trivy-db outage no longer turns build-docker red and skips the snapshot
  deploy; a 15-minute step timeout bounds the Java DB download, which
  trivy's own --timeout does not.
- Install Trivy v0.70.0 with its own setup-trivy step (cache: true) and run
  trivy-action with skip-setup-trivy: true: its cache: false also skipped
  the binary cache and left an anonymous github.com lookup in every run.
- docker-scan.yml: timeout-minutes: 30; document the neutral
  "1 configuration not found" PR check and the linux/amd64-only scan;
  "gate" -> "scan".
- Both: persist-credentials: false on checkout; upload on !cancelled()
  instead of always().
@vharseko
vharseko force-pushed the features/docker-trivy-scan branch from 957ea20 to 793334e Compare October 22, 2026 11:59
@vharseko

Copy link
Copy Markdown
Member Author

@maximthomas addressed in 793334e. Before that I rebased the branch onto current master. There were no conflicts, and git range-diff shows the two earlier commits unchanged. Per point:

Trivy outage fails build-docker (question). It is a bug: "findings do not fail the build" was meant as "the scan never fails the build". OpenIDM#216 and OpenICF#135 settled the same question, and this round follows them. The build.yml scan step is now continue-on-error, with a comment that names the deploy.yml consequence. The same change exposed a second problem, which OpenICF#135 hit: with cache: false, trivy-action also skips setup-trivy's binary cache, so every run does an anonymous github.com release lookup. In this PR's run the log shows setup-trivy@3fb12ec with cache: false. In OpenICF run 37051597083 that lookup failed (crit unable to find 'v0.70.0'), and under continue-on-error the scan was lost without a trace. Trivy v0.70.0 (the action's default) is therefore installed by its own aquasecurity/setup-trivy step (81e5143…, v0.3.1, cache: true), and trivy-action runs with skip-setup-trivy: true. cache: false stays for the DBs. In build.yml the install step is continue-on-error as well. docker-scan.yml gets the same install step, but without continue-on-error, because a failed weekly scan is the signal there.

docker-scan.yml is never executed before merge (suggestion). I took your second option, as in OpenICF#135: I will run gh workflow run docker-scan.yml right after merge and report the run here. I did not add a permanent pull_request trigger. Every PR's build.yml scan already exercises the same trivy-action and setup-trivy SHAs, Dependabot bumps included. What only the weekly run covers is the Docker Hub pull, the format() guard and the trivy-image-latest upload, and the post-merge run checks those once.

No timeout-minutes (suggestion). Done: timeout-minutes: 30 on the docker-scan.yml job. I checked trivy v0.70.0, and pkg/javadb/client.go:49 does build its context from context.Background(). The same hang could hold build-docker for 360 minutes and then fail it anyway, so the build.yml scan step also gets a step-level timeout-minutes: 15. Together with continue-on-error, a stalled Java DB fetch now costs at most 15 minutes and leaves the job green.

always() vs. the comment (suggestion). Done in both upload steps: !cancelled() && hashFiles(...). The comments now say "but not on a cancelled run or without a report to upload".

persist-credentials (suggestion). Done: persist-credentials: false on both new checkouts.

Mutable tags on checkout@v7 / upload-sarif@v4 (suggestion). No change here. #170 does not pin these: its description says "actions/* and github/codeql-action stay on major tags (first-party, not flagged by CodeQL)". At its head, build.yml still has checkout@v7, setup-java@v6, cache@v6, upload-artifact@v7 and download-artifact@v8, and codeql.yml has codeql-action/init@v4 and analyze@v4. So after both PRs land, these four lines are not the only mutable tags. They follow the same policy, and the third-party aquasecurity/* actions are SHA-pinned. If first-party actions should be pinned too, that is a policy change for #170 or a follow-up across all workflows, not something to do in four lines here.

Description vs. checks (note). Rewritten. It now names the Trivy check (one per SARIF tool; trivy-build-default is only the category) and says that the check is diff-scoped: this PR's check-run reads "No new alerts in code changed by this pull request", and the findings are under pr:181 tool:Trivy.

Also taken over from the sibling rounds, though not raised here:

  • docker-scan.yml header: once the weekly scan has run, every PR's Trivy check concludes neutral with "1 configuration not found". OpenAM PRs #1184–#1192 show exactly that today.
  • The docker-scan.yml comment now says "build.yml scan (ignore-unfixed: true)" instead of "gate".
  • The docker-scan.yml comment now says that only the linux/amd64 manifest is scanned (no --platform), not arm64/ppc64le/s390x.

The description is also corrected on the actions cache: it is at 2.4 GiB today, not at the quota. cache: false still stands, because the trivy DBs are ~1 GB per branch scope and day.

@vharseko
vharseko requested a review from maximthomas October 22, 2026 12:00

@maximthomas maximthomas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: The new commit makes the scan genuinely non-fatal on the outage it targets, and the comments explaining it are accurate.

  • continue-on-error + timeout-minutes: 15 on the Scan step (.github/workflows/build.yml:141-142): actions/runner v2.338.0, the version this job ran, records a composite step's timeout as Failed and continue-on-error converts it, so a stalled Java DB fetch costs at most 15 minutes and leaves build-docker green.
  • The hashFiles('trivy-results.sarif') != '' guard (build.yml:157) holds on that path: trivy v0.70.0 creates --output only after a successful scan (pkg/report/writer.go:35 via runner.Report), so a DB outage skips the upload instead of failing it on an empty SARIF.
  • "trivy's own --timeout does not bound the Java DB download" is right (pkg/javadb/client.go:49 runs it under context.Background()). So is "linux/amd64 only", on both the daemon path in build.yml and the remote path in docker-scan.yml (go-containerregistry's default platform).

@vharseko
vharseko merged commit 55fd54d into OpenIdentityPlatform:master Oct 22, 2026
15 checks passed
@vharseko
vharseko deleted the features/docker-trivy-scan branch October 22, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Build, CI/CD, and GitHub Actions changes dependencies Pull requests that update a dependency file docker Docker image and container changes security Security fixes and CVE / vulnerability updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants