From 07f40c1b645e4cbf7cb2acff6c329dc8a2e9c1f1 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 18 Sep 2026 19:30:17 +0300 Subject: [PATCH 1/4] Add Trivy vulnerability scanning for Docker images 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 #130. --- .github/workflows/build.yml | 52 +++++++++++++++++++++++++++ .github/workflows/docker-scan.yml | 59 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 .github/workflows/docker-scan.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f0ced25d..7efaf147a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -124,6 +124,9 @@ jobs: !**/*-sources.jar build-docker: runs-on: 'ubuntu-latest' + permissions: + contents: read + security-events: write services: registry: image: registry:2 @@ -147,6 +150,7 @@ jobs: echo "last release: $release_version" test -n "$release_version" echo "release_version=$release_version" >> "$GITHUB_ENV" + echo "image_repository=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 @@ -179,8 +183,33 @@ jobs: docker run --rm -it -d --memory="1g" --name=test localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test | grep -q \"healthy\"; do sleep 10; done' docker logs test + - 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 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + limit-severities-for-sarif: true + ignore-unfixed: true + scanners: vuln + cache: false + - name: Upload Trivy report to GitHub Security + uses: github/codeql-action/upload-sarif@v4 + # upload even if a preceding step failed, but not without a report to upload + if: ${{ always() && hashFiles('trivy-results.sarif') != '' }} + with: + sarif_file: trivy-results.sarif + # distinct from the docker-scan.yml categories, which track the published images + category: trivy-build-default build-docker-alpine: runs-on: 'ubuntu-latest' + permissions: + contents: read + security-events: write services: registry: image: registry:2 @@ -204,6 +233,7 @@ jobs: echo "last release: $release_version" test -n "$release_version" echo "release_version=$release_version" >> "$GITHUB_ENV" + echo "image_repository=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 @@ -237,3 +267,25 @@ jobs: docker run --rm -it -d --memory="1g" --name=test localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test | grep -q \"healthy\"; do sleep 10; done' docker logs test + - 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 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }}-alpine + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + limit-severities-for-sarif: true + ignore-unfixed: true + scanners: vuln + cache: false + - name: Upload Trivy report to GitHub Security + uses: github/codeql-action/upload-sarif@v4 + # upload even if a preceding step failed, but not without a report to upload + if: ${{ always() && hashFiles('trivy-results.sarif') != '' }} + with: + sarif_file: trivy-results.sarif + # distinct from the docker-scan.yml categories, which track the published images + category: trivy-build-alpine diff --git a/.github/workflows/docker-scan.yml b/.github/workflows/docker-scan.yml new file mode 100644 index 000000000..60f702599 --- /dev/null +++ b/.github/workflows/docker-scan.yml @@ -0,0 +1,59 @@ +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. + +# 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. +name: Docker Scan + +on: + schedule: + - cron: '30 5 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + scan: + # Do not run the scheduled scan in forks; manual runs are always allowed. + if: github.event_name == 'workflow_dispatch' || github.repository == 'OpenIdentityPlatform/OpenICF' + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + tag: [ 'latest', 'alpine' ] + steps: + - uses: actions/checkout@v7 + - name: Scan openidentityplatform/openicf:${{ matrix.tag }} (Trivy) + # unlike the build.yml gate, unfixed CVEs are reported too: surfacing them in + # already-released images is the point of this workflow + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: openidentityplatform/openicf:${{ matrix.tag }} + format: sarif + output: trivy-${{ matrix.tag }}.sarif + severity: CRITICAL,HIGH + limit-severities-for-sarif: true + scanners: vuln + cache: false + - name: Upload report to GitHub Security + uses: github/codeql-action/upload-sarif@v4 + # upload even if a preceding step failed, but not without a report to upload + if: ${{ always() && hashFiles(format('trivy-{0}.sarif', matrix.tag)) != '' }} + with: + sarif_file: trivy-${{ matrix.tag }}.sarif + category: trivy-image-${{ matrix.tag }} From c4b8772dbee7288ae947ba2f00161a85cad5affc Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 2 Oct 2026 14:41:02 +0300 Subject: [PATCH 2/4] Build the Docker images in build.yml from the ZIP of the commit under 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. --- .github/workflows/build.yml | 24 ++++++++++++++++++++++++ Dockerfile | 4 +++- Dockerfile-alpine | 4 +++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7efaf147a..b1a01b923 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,6 +123,7 @@ jobs: OpenICF-xml-connector/target/*.jar !**/*-sources.jar build-docker: + needs: build-maven runs-on: 'ubuntu-latest' permissions: contents: read @@ -136,6 +137,17 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + name: ubuntu-latest-11 + - name: Prepare Dockerfile + # build the image from the ZIP of this commit, not from the last release + shell: bash + run: | + sed -i -E '/^#COPY OpenICF/s/^#//' ./Dockerfile + grep '^COPY OpenICF-java-framework/openicf-zip/target/' ./Dockerfile + ls -l OpenICF-java-framework/openicf-zip/target/*.zip # 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 is what the Dockerfile's releases/download URL needs, and that @@ -206,6 +218,7 @@ jobs: # distinct from the docker-scan.yml categories, which track the published images category: trivy-build-default build-docker-alpine: + needs: build-maven runs-on: 'ubuntu-latest' permissions: contents: read @@ -219,6 +232,17 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + name: ubuntu-latest-11 + - name: Prepare Dockerfile + # build the image from the ZIP of this commit, not from the last release + shell: bash + run: | + sed -i -E '/^#COPY OpenICF/s/^#//' ./Dockerfile-alpine + grep '^COPY OpenICF-java-framework/openicf-zip/target/' ./Dockerfile-alpine + ls -l OpenICF-java-framework/openicf-zip/target/*.zip # 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 is what the Dockerfile's releases/download URL needs, and that diff --git a/Dockerfile b/Dockerfile index e97f23376..5e68dd5ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,11 +22,13 @@ ARG VERSION WORKDIR /opt +# build.yml uncomments the COPY to build the image from the ZIP of the commit under test; +# without it the openicf-$VERSION.zip of the release is downloaded #COPY OpenICF-java-framework/openicf-zip/target/*.zip ./ RUN apt-get update \ && apt-get install -y --no-install-recommends curl unzip \ - && bash -c 'if [ ! -z "$VERSION" ] ; then rm -rf ./*.zip ; curl -L https://github.com/OpenIdentityPlatform/OpenICF/releases/download/$VERSION/openicf-$VERSION.zip --output openicf-$VERSION.zip ; fi' \ + && bash -c 'if [ ! -z "$VERSION" ] && ! ls ./openicf-*.zip >/dev/null 2>&1 ; then curl -L https://github.com/OpenIdentityPlatform/OpenICF/releases/download/$VERSION/openicf-$VERSION.zip --output openicf-$VERSION.zip ; fi' \ && unzip openicf-*.zip && rm -rf *.zip \ && apt-get remove -y --purge unzip \ && rm -rf /var/lib/apt/lists/* \ diff --git a/Dockerfile-alpine b/Dockerfile-alpine index 8b2573965..c54ba1da3 100644 --- a/Dockerfile-alpine +++ b/Dockerfile-alpine @@ -24,13 +24,15 @@ ARG TARGETARCH WORKDIR /opt +# build.yml uncomments the COPY to build the image from the ZIP of the commit under test; +# without it the openicf-$VERSION.zip of the release is downloaded #COPY OpenICF-java-framework/openicf-zip/target/*.zip ./ RUN apk add --update --no-cache --virtual builddeps curl unzip \ && apk upgrade --update --no-cache \ && if [ "$TARGETARCH" = "386" ]; then JDK=openjdk11-jre; else JDK=openjdk25-jre; fi \ && apk add bash "$JDK" \ - && bash -c 'if [ ! -z "$VERSION" ] ; then rm -rf ./*.zip ; curl -L https://github.com/OpenIdentityPlatform/OpenICF/releases/download/$VERSION/openicf-$VERSION.zip --output openicf-$VERSION.zip ; fi' \ + && bash -c 'if [ ! -z "$VERSION" ] && ! ls ./openicf-*.zip >/dev/null 2>&1 ; then curl -L https://github.com/OpenIdentityPlatform/OpenICF/releases/download/$VERSION/openicf-$VERSION.zip --output openicf-$VERSION.zip ; fi' \ && unzip openicf-*.zip && rm -rf *.zip \ && apk del unzip \ && addgroup -S $USER \ From 82fc1cec4a19df8fced72a0c2b6cc9161b7cd908 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 2 Oct 2026 14:41:18 +0300 Subject: [PATCH 3/4] Keep a Trivy outage from failing the Docker smoke test and document the 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. --- .github/workflows/build.yml | 10 ++++++++-- .github/workflows/docker-scan.yml | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1a01b923..c6f0b7ac2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -198,7 +198,10 @@ jobs: - 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 + # evicting the m2-repository caches out of the repo's 10GB actions-cache quota. + # Findings do not fail the step; a Trivy or trivy-db registry outage does, and + # must not fail the image smoke test above + continue-on-error: true uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }} @@ -294,7 +297,10 @@ jobs: - 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 + # evicting the m2-repository caches out of the repo's 10GB actions-cache quota. + # Findings do not fail the step; a Trivy or trivy-db registry outage does, and + # must not fail the image smoke test above + continue-on-error: true uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }}-alpine diff --git a/.github/workflows/docker-scan.yml b/.github/workflows/docker-scan.yml index 60f702599..ce54c3ef2 100644 --- a/.github/workflows/docker-scan.yml +++ b/.github/workflows/docker-scan.yml @@ -14,6 +14,8 @@ # 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". name: Docker Scan on: @@ -39,8 +41,10 @@ jobs: steps: - uses: actions/checkout@v7 - name: Scan openidentityplatform/openicf:${{ matrix.tag }} (Trivy) - # unlike the build.yml gate, unfixed CVEs are reported too: surfacing them in - # already-released images is the point of this workflow + # unlike the build.yml scan, unfixed CVEs are reported too: surfacing them in + # already-released images is the point of this workflow. Trivy pulls only the + # runner's linux/amd64 manifest; the other published platforms are not scanned + # (alpine on linux/386 ships openjdk11-jre instead of openjdk25-jre) uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: openidentityplatform/openicf:${{ matrix.tag }} From 5ee002128b1c967e67474df608b1af88eb928f2b Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Sun, 4 Oct 2026 10:47:16 +0300 Subject: [PATCH 4/4] Cache the Trivy binary and keep the docker jobs running past unrelated 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". --- .github/workflows/build.yml | 38 ++++++++++++++++++++++++++----- .github/workflows/docker-scan.yml | 17 ++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6f0b7ac2..4f7a84108 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,6 +110,8 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: + # 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 }} retention-days: 5 path: | @@ -124,6 +126,9 @@ jobs: !**/*-sources.jar 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() }} runs-on: 'ubuntu-latest' permissions: contents: read @@ -150,9 +155,9 @@ jobs: ls -l OpenICF-java-framework/openicf-zip/target/*.zip # 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 is what the Dockerfile's releases/download URL needs, and that - # URL is upstream's, so a fork build asks upstream too. `|| true` keeps a - # failed lookup going to the `last release:` line, and `test -n` stops it. + # 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. - name: Get latest release version shell: bash env: @@ -195,6 +200,14 @@ jobs: docker run --rm -it -d --memory="1g" --name=test localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test | grep -q \"healthy\"; do sleep 10; done' docker logs test + - 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) # 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 @@ -204,6 +217,7 @@ jobs: continue-on-error: true uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: + skip-setup-trivy: true image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }} format: sarif output: trivy-results.sarif @@ -222,6 +236,9 @@ jobs: category: trivy-build-default build-docker-alpine: 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() }} runs-on: 'ubuntu-latest' permissions: contents: read @@ -248,9 +265,9 @@ jobs: ls -l OpenICF-java-framework/openicf-zip/target/*.zip # 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 is what the Dockerfile's releases/download URL needs, and that - # URL is upstream's, so a fork build asks upstream too. `|| true` keeps a - # failed lookup going to the `last release:` line, and `test -n` stops it. + # 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. - name: Get latest release version shell: bash env: @@ -294,6 +311,14 @@ jobs: docker run --rm -it -d --memory="1g" --name=test localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test | grep -q \"healthy\"; do sleep 10; done' docker logs test + - 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) # 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 @@ -303,6 +328,7 @@ jobs: continue-on-error: true uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: + skip-setup-trivy: true image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }}-alpine format: sarif output: trivy-results.sarif diff --git a/.github/workflows/docker-scan.yml b/.github/workflows/docker-scan.yml index ce54c3ef2..38f91b511 100644 --- a/.github/workflows/docker-scan.yml +++ b/.github/workflows/docker-scan.yml @@ -14,8 +14,9 @@ # 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". +# 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". name: Docker Scan on: @@ -40,13 +41,21 @@ jobs: tag: [ 'latest', 'alpine' ] steps: - uses: actions/checkout@v7 + - 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 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # v0.2.6 + with: + version: v0.70.0 + cache: true - name: Scan openidentityplatform/openicf:${{ matrix.tag }} (Trivy) # unlike the build.yml scan, unfixed CVEs are reported too: surfacing them in # already-released images is the point of this workflow. Trivy pulls only the - # runner's linux/amd64 manifest; the other published platforms are not scanned - # (alpine on linux/386 ships openjdk11-jre instead of openjdk25-jre) + # linux/amd64 manifest (no --platform is passed); the other published platforms + # are not scanned (alpine on linux/386 ships openjdk11-jre instead of openjdk25-jre) uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: + skip-setup-trivy: true image-ref: openidentityplatform/openicf:${{ matrix.tag }} format: sarif output: trivy-${{ matrix.tag }}.sarif