Skip to content

Add Trivy vulnerability scanning for Docker images - #135

Merged
vharseko merged 4 commits into
OpenIdentityPlatform:masterfrom
vharseko:docker-trivy-scan
Oct 5, 2026
Merged

vharseko merged 4 commits into
OpenIdentityPlatform:masterfrom
vharseko:docker-trivy-scan

Conversation

@vharseko

@vharseko vharseko commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Adds Docker image vulnerability scanning with Trivy, mirroring OpenIdentityPlatform/OpenDJ#854.

build.yml — build-docker and build-docker-alpine now build the image from the ZIP of the commit under test, as OpenDJ does: they wait for build-maven, take openicf-*.zip from its ubuntu-latest-11 artifact and uncomment the COPY in the Dockerfile. Before, the image was built from the last release's ZIP (VERSION=release_version), so neither the smoke test nor a scan saw this commit's jars. Dockerfile / Dockerfile-alpine now download the release ZIP only when none was copied in, so release.yml is unchanged. They run unless the run was cancelled (if: ${{ !cancelled() }}), so a red matrix leg other than ubuntu-latest-11 no longer skips the image smoke test and scan; a red ubuntu-latest-11 leg fails them at the artifact download. A docker-job re-run after the artifact's 5-day retention needs "Re-run all jobs".

Both jobs scan the freshly built image (resolved from the local Docker daemon, so the runner's linux/amd64 manifest only) right after the Docker test step. Findings do not fail the build: the SARIF report is uploaded via codeql-action/upload-sarif, so PRs get a Trivy code-scanning check next to CodeQL (one check-run per SARIF tool, covering both the trivy-build-default and trivy-build-alpine categories), and the full list lives in the Security tab. The scan step is continue-on-error, so a Trivy or trivy-db registry outage does not fail the image smoke test. 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 action's built-in ~1GB DB cache is disabled (cache: false) so it cannot evict the m2-repository caches out of the repo's 10GB actions-cache quota. Because trivy-action passes that same cache input to setup-trivy, which would then skip its binary cache and do an anonymous github.com release lookup in every job, Trivy is installed by a separate aquasecurity/setup-trivy step (version: v0.70.0, cache: true, also continue-on-error) and trivy-action runs with skip-setup-trivy: true. The two docker jobs get security-events: write next to contents: read; build.yml's workflow-level permissions: (from #130) stays contents: read for every other job.

docker-scan.yml (new) — weekly cron (30 5 * * 1) + workflow_dispatch scan of the published openidentityplatform/openicf:latest and :alpine images: new CVEs surface in already-released images (mostly via the base image) without any change in this repository. Unlike the build-time scan, unfixed CVEs are reported too. Trivy is installed the same way (separate setup-trivy step with the binary cached, here without continue-on-error). Only the linux/amd64 manifest is scanned (no --platform is passed, so go-containerregistry's default applies); the other published platforms are not (alpine on linux/386 ships openjdk11-jre instead of openjdk25-jre). Reports are uploaded as SARIF with a separate category per tag (trivy-image-*, distinct from the trivy-build-* categories in build.yml). The scheduled run is skipped in forks; manual runs are always allowed. Its triggers need the file on the default branch, so nothing on this PR runs it: it is to be started once by hand (gh workflow run docker-scan.yml) right after merge. Since the trivy-image-* categories exist only on master, every PR that uploads both trivy-build-* categories will then get a Trivy check that concludes neutral with "2 configurations not found" (as on OpenDJ).

aquasecurity/trivy-action (v0.36.0) and aquasecurity/setup-trivy (v0.2.6, the same SHA trivy-action itself uses) are pinned by commit SHA, matching the pinning convention #130 introduced for third-party actions in this repo. Future false positives / accepted findings can be suppressed via a .trivyignore file in the repository root or dismissed in the Security tab.

@vharseko vharseko added ci CI, build & workflow changes docker security Security fix / CVE remediation labels 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 in carefully.

  • limit-severities-for-sarif: true next to severity: CRITICAL,HIGH, so the severity filter also applies to SARIF output.
  • security-events: write is granted only to build-docker / build-docker-alpine, with contents: read, and the upload is guarded by if: ${{ always() && hashFiles('trivy-results.sarif') != '' }}.
  • aquasecurity/trivy-action is pinned by SHA (ed142fd… # v0.36.0), and docker-scan.yml skips the cron in forks while still allowing workflow_dispatch.

issue (non-blocking): Once docker-scan.yml has run on master, every PR's Trivy check will end neutral with "2 configurations not found".

.github/workflows/docker-scan.yml:59, .github/workflows/build.yml:192, :267

trivy-image-latest / trivy-image-alpine are uploaded only on master, while PRs upload only trivy-build-* under the same tool. GitHub compares a PR against every configuration of that tool on the base branch. OpenDJ, which has the same setup, shows the result: the Trivy check-run on OpenDJ PR #1152 is neutral, titled "2 configurations not found", with the summary "Code scanning cannot determine the alerts introduced by this pull request, because 2 configurations present on refs/heads/master were not found". The PR check this change adds would show that warning on every PR from then on. At minimum, say so where the next reader will look:

# Scans the published Docker images for known vulnerabilities: new CVEs surface in
# already-released images (mostly via the base image), without any change in this repository.
# Its trivy-image-* categories exist only on master, so once it has run, every PR's
# "Trivy" code-scanning check concludes neutral with "2 configurations not found".

question (non-blocking): Was the build-time scan meant to catch CVEs in Maven dependencies that a PR adds?

.github/workflows/build.yml:171-184, :246-259; Dockerfile:25-30, Dockerfile-alpine:27-34

The scanned image is built with VERSION=${{ env.release_version }}, and the Dockerfiles download openicf-$VERSION.zip from the latest GitHub release; the COPY OpenICF-java-framework/openicf-zip/target/*.zip line is commented out. The image therefore never contains the PR's jars, so a PR that adds a dependency with a fixable CRITICAL/HIGH CVE still gets a green Trivy check. Only Dockerfile and base-image changes are scanned. If catching such dependencies was the goal, this is a Major gap and needs a scan of the built ZIP, e.g. scan-type: fs over OpenICF-java-framework/openicf-zip/target in build-maven. If it was not, one comment line is enough:

      - name: Scan image for vulnerabilities (Trivy)
        # the image is built from the last release's ZIP (VERSION=release_version), so this
        # scan covers the Dockerfile and the base image, not this commit's jars;
        # trivy resolves the image from the local Docker daemon, so only the runner's

suggestion (non-blocking): Make the scan step continue-on-error, so that a Trivy or registry outage does not fail the image smoke test.

.github/workflows/build.yml:171, :246

When there are findings the step exits 0 (no exit-code). It exits 1 when the trivy binary download fails, or when the trivy-db / trivy-java-db download (~923 MiB, fetched again on every run because of cache: false) fails on both mirror.gcr.io and ghcr.io. build-docker / build-docker-alpine are the only smoke test of the image, and they would then go red even though the image is fine. The upload's if: always() && hashFiles(...) already skips a missing report.

        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        # a Trivy or DB-registry outage must not fail the image smoke test
        continue-on-error: true

suggestion (non-blocking): Say that the published-image scan covers only linux/amd64. The alpine image ships a different JRE on linux/386.

.github/workflows/docker-scan.yml:41-46; Dockerfile-alpine:31

No platform is given, so Trivy pulls only the runner's linux/amd64 manifest. release.yml publishes alpine for 6 platforms, and Dockerfile-alpine:31 installs openjdk11-jre on 386 and openjdk25-jre on all the others. A CVE in the 386 JRE is therefore never reported. build.yml states its amd64-only limit; this file does not.

      - name: Scan openidentityplatform/openicf:${{ matrix.tag }} (Trivy)
        # trivy pulls only the runner's linux/amd64 manifest; the other published platforms
        # (alpine on linux/386 ships openjdk11-jre instead of openjdk25-jre) are not scanned.

Or: add a matrix entry for alpine on linux/386, with the step env TRIVY_PLATFORM: linux/386 and its own category (untested).


suggestion (non-blocking): Run docker-scan.yml once before merge. Nothing on this PR runs it.

.github/workflows/docker-scan.yml:19-22

Its only triggers are schedule and workflow_dispatch, and both need the file on the default branch. gh run list --workflow docker-scan.yml returns 404 upstream and on vharseko/OpenICF. The Docker Hub pull, the templated SARIF names, the hashFiles(format(...)) guard and the trivy-image-* upload will first run on the Monday after merge. OpenDJ's twin workflow has been green for 4 weeks, so the risk is low.

# after the file is on vharseko/OpenICF's default branch (or on upstream right after merge)
gh workflow run docker-scan.yml -R vharseko/OpenICF
gh run list -R vharseko/OpenICF --workflow docker-scan.yml --limit 1

Pin: a green run that uploads two trivy-image-* analyses.


nitpick (non-blocking): The comment calls build.yml's scan a "gate", but it gates nothing.

.github/workflows/docker-scan.yml:42

build.yml sets no exit-code. On this PR, 2+2 findings were uploaded and Trivy still passed.

        # unlike the build.yml scan, unfixed CVEs are reported too: surfacing them in

nitpick (non-blocking): The PR description promises a "Code scanning results / trivy-build-*" check; the actual check is Trivy.

.github/workflows/build.yml:185-192

GitHub creates one check-run per SARIF tool (Trivy, next to CodeQL), covering both trivy-build-* categories. A reviewer or a branch-protection rule looking for trivy-build-* will not find it.

@vharseko

vharseko commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

@maximthomas the branch is rebased onto current master; the answers per point:

"2 configurations not found" (issue). Confirmed on OpenDJ #1152. Documented in the header of docker-scan.yml, as you suggested, and in the PR description (6bc5403). The neutral check itself stays: making it go away means giving the published-image SARIF a different tool name, which is untested and would diverge from OpenDJ.

Build-time scan and the PR's Maven dependencies (question). Yes, it should cover them — fixed rather than commented (1e5ec91). As in OpenDJ, build-docker / build-docker-alpine now need build-maven, take openicf-*.zip from its ubuntu-latest-11 artifact and uncomment the COPY in the Dockerfile, so both the smoke test and the Trivy scan see this commit's jars. Dockerfile / Dockerfile-alpine download the release ZIP only when none was copied in, so release.yml builds exactly as before. The cost: the docker jobs now start after the whole build-maven matrix.

continue-on-error on the scan step (suggestion). Done in both jobs (6bc5403).

Published-image scan is linux/amd64 only (suggestion). Documented in docker-scan.yml, including the openjdk11-jre on alpine/linux/386 (6bc5403). No extra matrix entry for now.

Run docker-scan.yml once before merge (suggestion). Not done before merge: it will be started by hand (gh workflow run docker-scan.yml) right after merge, as the PR description now says; I'll report the run here.

"gate" (nitpick). Now "scan" (6bc5403).

Check name in the PR description (nitpick). The description now names the Trivy check (one check-run per SARIF tool, covering both trivy-build-* categories) and describes the build-from-ZIP change.

@vharseko
vharseko requested a review from maximthomas October 2, 2026 11:43
Scans the freshly built images (default and alpine) in build.yml for
fixable CRITICAL/HIGH CVEs and uploads SARIF to code scanning, and adds
a weekly docker-scan workflow that scans the published
openidentityplatform/openicf:latest/:alpine images. Mirrors
OpenIdentityPlatform/OpenDJ#854, with aquasecurity/trivy-action pinned
by commit SHA to match the pinning convention introduced in OpenIdentityPlatform#130.
… test

build-docker and build-docker-alpine now wait for build-maven, take the
openicf ZIP from its ubuntu-latest-11 artifact and uncomment the COPY in
the Dockerfile, so the smoke test and the Trivy scan cover this commit's
jars instead of the last release. The Dockerfiles download the release
ZIP only when none was copied in, so release.yml is unchanged.
…he scan limits

The build-time scan step is continue-on-error: findings never fail it, only
a Trivy or trivy-db registry outage does. docker-scan.yml now states that it
scans only the linux/amd64 manifest and that its master-only trivy-image-*
categories leave every PR's Trivy check neutral, and no longer calls the
build.yml scan a gate.
@vharseko

vharseko commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

@maximthomas the branch is rebased onto current master (c7648cf) again: #130 landed after the previous round and conflicted with this PR in build.yml. No change beyond the conflict resolution.

Conflict in build-docker / build-docker-alpine. #130 rewrote the "Get latest release version" step: gh release view with GH_TOKEN and a test -n guard instead of the anonymous curl. That step is kept as on master; this PR only adds its image_repository=${GITHUB_REPOSITORY,,} line to it, which the Trivy step's image-ref needs. The new "Download artifacts" / "Prepare Dockerfile" steps sit in front of #130's comment block, so the comment stays right above the step it describes.

Commit hashes cited in my previous reply. After the rebase: 1e5ec91 → c4b8772 (build from the ZIP of the commit under test), 6bc5403 → 82fc1ce (continue-on-error, the docs and "gate" → "scan"). git range-diff shows only master context changes in the first two commits; the third is identical.

PR description. build.yml now has a workflow-level permissions: contents: read from #130, so the description no longer says there is none, and #130 is referred to as merged.

@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 docker jobs now test and scan this commit, and the release path is unchanged.

  • Prepare Dockerfile + the ubuntu-latest-11 artifact build the images from this commit's ZIP. The scan shows it: refs/pull/135/merge now has alerts for the PR's own bcprov-jdk18on 1.84 and bc-fips 2.1.2.
  • The ls ./openicf-*.zip guard in Dockerfile / Dockerfile-alpine keeps release.yml's download path as it was. release.yml passes only VERSION, and its COPY stays commented.
  • Every round-1 point is addressed: continue-on-error on both scans, the amd64-only note, the "2 configurations not found" note, and the Trivy check named in the description.

issue (non-blocking): cache: false also turns off setup-trivy's binary cache, so every job downloads Trivy anonymously from github.com. On this head the alpine scan never ran, but the job stayed green.

.github/workflows/build.yml:214, :313, .github/workflows/docker-scan.yml:55

trivy-action@ed142fd passes cache on to setup-trivy@3fb12ec (action.yaml:129-133). With cache: false, the binary is never restored from cache, and install.sh's checking GitHub for tag lookup runs in every job (token-setup-trivy only authenticates the checkout). In run 37051597083, build-docker-alpine logged crit unable to find 'v0.70.0'. continue-on-error kept the job green, no trivy-results.sarif was written, and the upload was skipped. refs/pull/135/merge has only trivy-build-default alerts, but the Trivy check passed. So 1 of 2 scans was lost on this head. The step comment says cache: false only keeps the DBs out of the cache.

      - name: Install Trivy
        # cached, unlike the DBs: trivy-action's `cache: false` would also skip the binary
        # cache and leave an anonymous github.com release lookup in every run
        continue-on-error: true
        uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # v0.2.6
        with:
          version: v0.70.0
          cache: true
      - name: Scan image for vulnerabilities (Trivy)
        continue-on-error: true
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          skip-setup-trivy: true
          # ...inputs as now, cache: false kept for the DBs

Not run. The first run for each version still does the lookup. docker-scan.yml needs the same change.


suggestion (non-blocking): Let the docker jobs run when an unrelated build-maven leg fails.

.github/workflows/build.yml:126, :224

needs: build-maven with no if: waits on all 9 legs of the fail-fast: false matrix. A single red macOS, Windows or other-JDK leg then skips both docker jobs: the image build, the smoke test, the scan and the upload. In 5 of the 7 recent red Build runs, ubuntu-latest-11 was green and another leg failed (e.g. 36698693947 windows-26, 37002960094 windows-11). At BASE the docker jobs ran in those runs.

  build-docker:
    needs: build-maven
    # run even when an unrelated matrix leg failed; the download below still
    # fails if the ubuntu-latest-11 leg itself did
    if: ${{ !cancelled() }}

Not run. The jobs still wait for the slowest leg, because GitHub cannot wait on a single matrix leg.


suggestion (non-blocking): Note at the upload step that the docker jobs depend on the ubuntu-latest-11 artifact.

.github/workflows/build.yml:113-114, :143, :241

If "Re-run failed jobs" is used on a docker job after retention-days: 5, only that job re-runs, and download-artifact fails with "artifact not found". At BASE the same re-run worked.

          # build-docker and build-docker-alpine download ubuntu-latest-11; a docker-job
          # re-run after retention-days needs "Re-run all jobs"
          name: ${{ matrix.os }}-${{ matrix.java }}

nitpick (non-blocking): The comment on Get latest release version still says the Dockerfile's download URL needs the tag.

.github/workflows/build.yml:153-155, :251-253

With the COPY uncommented, the Dockerfile skips the download (the logs unzip only openicf-2.1.0-SNAPSHOT.zip). The tag now only names the local image.

      # Authenticated: the anonymous api.github.com limit is per runner IP
      # and, once hit, the empty answer left the metadata step with no tag.
      # The tag only names the locally built image; the ZIP comes from build-maven.
      # `|| true` keeps a failed lookup going to the `last release:` line, and `test -n` stops it.

nitpick (non-blocking): The header says "every PR's" Trivy check reports 2 missing configurations. That holds only for a PR that uploads both trivy-build-* categories.

.github/workflows/docker-scan.yml:17-18

If a PR loses a scan (as on this head) or its docker jobs are skipped, it uploads fewer categories, and the count differs.

# Its trivy-image-* categories exist only on master, so once it has run, every PR that
# uploads both trivy-build-* categories gets a "Trivy" check that concludes neutral
# with "2 configurations not found".

nitpick (non-blocking): linux/amd64 is go-containerregistry's default, not the runner's platform.

.github/workflows/docker-scan.yml:45-46

There is no pull step, and no --platform is passed, so a remote multi-arch index resolves to the hard-coded linux/amd64. On ubuntu-latest the result is the same; only the stated reason is wrong.

        # already-released images is the point of this workflow. Trivy pulls only the
        # linux/amd64 manifest (no --platform is passed); the other published platforms are not scanned

…d build-maven failures

trivy-action's cache: false also skipped setup-trivy's binary cache, so every
job did an anonymous github.com release lookup; in run 37051597083 it failed
and the alpine scan was silently lost. Trivy is now installed by its own
setup-trivy step with the binary cached, while the DBs stay uncached.

build-docker and build-docker-alpine run unless the run was cancelled, so a
red leg other than ubuntu-latest-11 no longer skips the image smoke test and
scan. Comments: the docker jobs' dependency on the ubuntu-latest-11 artifact,
the release tag now only naming the local image, the linux/amd64 default in
docker-scan.yml and which PRs get "2 configurations not found".
@vharseko

vharseko commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

@maximthomas round 2 is addressed in 5ee0021, on top of the same base (c7648cfd, no rebase needed). Per point:

cache: false also disables the Trivy binary cache (issue). Confirmed: trivy-action@ed142fd passes cache to setup-trivy@3fb12ec, whose restore is gated on cache == 'true', so install.sh's anonymous github.com lookup ran in every job — and in run 37051597083 the alpine job died on it (crit unable to find 'v0.70.0') while the job stayed green. Fixed as you suggested, in both build.yml jobs and in docker-scan.yml: Trivy is installed by its own aquasecurity/setup-trivy step (same SHA as trivy-action uses, version: v0.70.0, 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 like the scan; in docker-scan.yml it is not, since a failed install there is exactly what that job should report. The first run per version (and per PR cache scope, until master has the entry) still does the lookup once.

For the record: OpenDJ has the same configuration and has not hit this in its last ~30 Build runs — but there the scan is not continue-on-error, so a failure would at least show as a red job; here it was silent.

Docker jobs skipped when an unrelated build-maven leg fails (suggestion). Done: both jobs now have if: ${{ !cancelled() }} with your comment. Checked against history: in 6 of the last 7 red Build runs here ubuntu-latest-11 was green, and OpenDJ shows the skip itself (e.g. 36986857906: only ubuntu-latest, 17 failed, both docker jobs skipped). A failed ubuntu-latest-11 leg still fails the docker jobs at the download step.

Re-run after retention-days (suggestion). Comment added at the Upload artifacts step.

Stale comment on Get latest release version (nitpick). Now says the tag only names the locally built image and the ZIP comes from build-maven. Side note, not changed here: that step's test -n can still stop the docker job on a failed lookup although the tag is now cosmetic; the step comes from #130 and mirrors release.yml, so I left it as is.

"every PR's" in the docker-scan.yml header (nitpick). Narrowed to PRs that upload both trivy-build-* categories.

linux/amd64 is go-containerregistry's default (nitpick). Fixed: the comment now says no --platform is passed (ggcr's defaultPlatform is hard-coded linux/amd64). The build.yml comments stay as they are — there the image is already in the local daemon after docker run.

@vharseko
vharseko requested a review from maximthomas October 4, 2026 08:06

@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: this round closes all of round 2's points, and the CI run at this head shows the fixes working.

  • Both scans now run: code scanning has trivy-build-default and trivy-build-alpine analyses for this head's merge commit (run 37187759719). At the previous head the alpine scan was lost.
  • if: ${{ !cancelled() }} on build-docker / build-docker-alpine (.github/workflows/build.yml:131, :241) keeps the image smoke test running when an unrelated matrix leg fails. A superseded run still skips both jobs.
  • The separate aquasecurity/setup-trivy step with cache: true (build.yml:203-210) skips the install-script checkout and the anonymous github.com tag lookup on a cache hit.

@vharseko
vharseko merged commit 438b89e into OpenIdentityPlatform:master Oct 5, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI, build & workflow changes docker security Security fix / CVE remediation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants