From bb5d9b76d9123e4db74ddd552c8259d948555bcf Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 3 Aug 2026 13:29:12 -0400 Subject: [PATCH 1/8] ci: add Trivy security scan to PR pipeline --- .github/steps/trivy/action.yml | 87 ++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 18 ++++++- .trivyignore | 3 ++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .github/steps/trivy/action.yml create mode 100644 .trivyignore diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml new file mode 100644 index 0000000000..ade53785e2 --- /dev/null +++ b/.github/steps/trivy/action.yml @@ -0,0 +1,87 @@ +name: "Trivy" +description: "PR security gate: scans repository dependencies (fs) and Dockerfile base images, prints findings and fails on HIGH/CRITICAL with a fix available" + +inputs: + maven-token: + description: "Token Maven uses to read internal artifacts from GitHub Packages (defaults to the workflow token, which cannot read other repos' packages)" + required: false + default: ${{ github.token }} + +runs: + using: composite + steps: + - name: Install Trivy + uses: aquasecurity/setup-trivy@v0.3.1 + with: + version: v0.73.0 + cache: true + + - name: Scan and evaluate + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.maven-token }} + GITHUB_ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + + # --- maven: resolve deps into ~/.m2 via Google's Central mirror so trivy + # can analyze parent-managed versions fully offline (no rate limits) --- + if [ -f pom.xml ]; then + printf '%s\n' \ + '' \ + ' google-central' \ + ' Google mirror of Maven Central' \ + ' https://maven-central.storage-download.googleapis.com/maven2/' \ + ' central' \ + '' > /tmp/central-mirror.xml + margs=(-B -q -fn -DskipTests -gs /tmp/central-mirror.xml dependency:go-offline) + [ -f .mvn/settings.xml ] && margs+=(-s .mvn/settings.xml) + mvn "${margs[@]}" || echo "::warning::Maven dependency resolution incomplete; pom.xml scan depth may be reduced" + fi + + # --- fs: lockfile dependencies --- + trivy fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan \ + --format json -o trivy-report.json . + jq '.Results = ((.Results // []) | map(. + {ScanSrc: "fs"}))' trivy-report.json > combined.json + + # --- image: base images from Dockerfiles --- + find . \( -name 'Dockerfile' -o -name 'Dockerfile.*' -o -name 'Dockerfile-*' \) -not -path '*/node_modules/*' -not -path './.git/*' > dfs.txt || : > dfs.txt + : > aliases.txt + : > imgs.txt + while IFS= read -r df; do + awk 'toupper($1)=="FROM" { if (toupper($3)=="AS") print $4; if (toupper($4)=="AS") print $5 }' "$df" >> aliases.txt + awk 'toupper($1)=="FROM" { i=$2; if (i ~ /^--/) i=$3; print i }' "$df" >> imgs.txt + done < dfs.txt + sort -u imgs.txt -o imgs.txt + while IFS= read -r img; do + [ -z "$img" ] && continue + [ "$img" = "scratch" ] && continue + case "$img" in *'$'*) continue ;; esac + grep -qxF "$img" aliases.txt && continue + echo "Scanning base image ${img}" + if trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --format json -o img.json "$img"; then + jq --arg s "$img" '.Results = ((.Results // []) | map(. + {ScanSrc: ("image " + $s)}))' img.json > img2.json + jq -s '{Results: (.[0].Results + (.[1].Results // []))}' combined.json img2.json > tmp.json && mv tmp.json combined.json + else + echo "skip ${img}" + fi + done < imgs.txt + + # --- report (deduplicated: one row per unique vulnerability, with occurrence count) --- + total=$(jq '[.Results[]?.Vulnerabilities[]?] | length' combined.json) + uniq=$(jq '[.Results[]? as $r | $r.Vulnerabilities[]? + | [.Severity, ($r.ScanSrc // "fs"), .PkgName, .VulnerabilityID]] | unique | length' combined.json) + if [ "$total" -gt 0 ]; then + echo "" + echo "=== HIGH/CRITICAL findings: ${uniq} unique (${total} occurrences across modules/images) ===" + jq -r '[.Results[]? as $r | $r.Vulnerabilities[]? + | {s: .Severity, ty: ($r.ScanSrc // "fs"), p: .PkgName, i: .InstalledVersion, + f: (.FixedVersion // "-"), id: .VulnerabilityID}] + | group_by([.s, .ty, .p, .id]) | map(.[0] + {n: length}) | sort_by(.s, .ty, .p) + | .[] | [.s, .ty, .p, .i + " -> " + .f, .id, "x\(.n)"] | @tsv' combined.json \ + | column -t -s "$(printf '\t')" + echo "" + echo "::error::Trivy found ${uniq} unique HIGH/CRITICAL vulnerabilities (${total} occurrences, fs + base images)" + exit 1 + fi + echo "No HIGH/CRITICAL vulnerabilities found." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1a28ec5915..05ebe7fcc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,22 @@ jobs: !github.event.pull_request.draft + trivy_scan: + name: "Trivy Scan" + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + if: github.event_name == 'pull_request' && github.event.pull_request.state == 'open' && !github.event.pull_request.draft + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Trivy scan (fs + base images) + uses: ./.github/steps/trivy + test: name: "Test meshcentral" needs: [changes] @@ -127,7 +143,7 @@ jobs: all-checks: name: "All Checks" - needs: [changes, test, test_helm] + needs: [changes, trivy_scan, test, test_helm] runs-on: ubuntu-latest if: always() steps: diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000000..60d72f6130 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,3 @@ +# Vulnerabilities to skip in the Trivy scan: one CVE/GHSA id per line, e.g.: +# +# CVE-2026-12345 From 80a46bd07d889f5755f7f6eb7d4796a8ee4fa053 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 3 Aug 2026 13:45:46 -0400 Subject: [PATCH 2/8] =?UTF-8?q?ci:=20harden=20trivy=20gate=20=E2=80=94=20n?= =?UTF-8?q?o=20persisted=20git=20credentials,=20visible=20warnings=20for?= =?UTF-8?q?=20skipped=20scans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/steps/trivy/action.yml | 9 ++++++--- .github/workflows/test.yml | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index ade53785e2..5975ad101d 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -36,7 +36,10 @@ runs: '' > /tmp/central-mirror.xml margs=(-B -q -fn -DskipTests -gs /tmp/central-mirror.xml dependency:go-offline) [ -f .mvn/settings.xml ] && margs+=(-s .mvn/settings.xml) - mvn "${margs[@]}" || echo "::warning::Maven dependency resolution incomplete; pom.xml scan depth may be reduced" + mvn "${margs[@]}" | tee /tmp/mvn.log || true + if grep -q '\[ERROR\]' /tmp/mvn.log; then + echo "::warning::Some Maven dependencies could not be resolved; pom.xml scan depth may be reduced" + fi fi # --- fs: lockfile dependencies --- @@ -56,14 +59,14 @@ runs: while IFS= read -r img; do [ -z "$img" ] && continue [ "$img" = "scratch" ] && continue - case "$img" in *'$'*) continue ;; esac + case "$img" in *'$'*) echo "::warning::Skipping base image with unresolved variable: ${img}"; continue ;; esac grep -qxF "$img" aliases.txt && continue echo "Scanning base image ${img}" if trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --format json -o img.json "$img"; then jq --arg s "$img" '.Results = ((.Results // []) | map(. + {ScanSrc: ("image " + $s)}))' img.json > img2.json jq -s '{Results: (.[0].Results + (.[1].Results // []))}' combined.json img2.json > tmp.json && mv tmp.json combined.json else - echo "skip ${img}" + echo "::warning::Base image ${img} could not be scanned (pull/scan error); it was NOT checked" fi done < imgs.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05ebe7fcc2..c7682616c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,6 +51,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Trivy scan (fs + base images) uses: ./.github/steps/trivy From 077837c6007ceddef2cb25a08392770f06ba482e Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Wed, 5 Aug 2026 08:43:48 -0400 Subject: [PATCH 3/8] ci: make trivy gate non-blocking (allow fail) until devs have capacity --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7682616c7..c9ff7df91d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -144,7 +144,7 @@ jobs: all-checks: name: "All Checks" - needs: [changes, trivy_scan, test, test_helm] + needs: [changes, test, test_helm] runs-on: ubuntu-latest if: always() steps: From 9dd46bf8b8858ca33dc588625020cc89fc4f2592 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Wed, 5 Aug 2026 14:09:29 -0400 Subject: [PATCH 4/8] ci: trivy code+docker scan in one job, in-build image scan, per-scan TSV artifacts --- .github/steps/trivy/action.yml | 147 +++++++++++++++++++-------------- .github/workflows/test.yml | 10 ++- 2 files changed, 91 insertions(+), 66 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 5975ad101d..f0b05da5cf 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -1,11 +1,23 @@ name: "Trivy" -description: "PR security gate: scans repository dependencies (fs) and Dockerfile base images, prints findings and fails on HIGH/CRITICAL with a fix available" +description: "Trivy PR security scans. scan: code = repo dependencies (HIGH/CRITICAL) + Dockerfile base images (CRITICAL), image = image built in the pipeline (HIGH/CRITICAL). Each scan uploads its TSV report as an artifact" inputs: + scan: + description: "code | image" + required: true maven-token: - description: "Token Maven uses to read internal artifacts from GitHub Packages (defaults to the workflow token, which cannot read other repos' packages)" + description: "Token for internal GitHub Packages: Maven resolution (scan: code) and docker build secret (scan: image)" required: false default: ${{ github.token }} + image-name: + description: "Service name from the build matrix (scan: image)" + required: false + dockerfile: + description: "Path to the service Dockerfile (scan: image)" + required: false + context: + description: "Docker build context (scan: image)" + required: false runs: using: composite @@ -16,75 +28,86 @@ runs: version: v0.73.0 cache: true - - name: Scan and evaluate + - name: Scan dependencies and Dockerfile base images + if: inputs.scan == 'code' shell: bash env: GITHUB_TOKEN: ${{ inputs.maven-token }} GITHUB_ACTOR: ${{ github.actor }} run: | set -euo pipefail - - # --- maven: resolve deps into ~/.m2 via Google's Central mirror so trivy - # can analyze parent-managed versions fully offline (no rate limits) --- - if [ -f pom.xml ]; then - printf '%s\n' \ - '' \ - ' google-central' \ - ' Google mirror of Maven Central' \ - ' https://maven-central.storage-download.googleapis.com/maven2/' \ - ' central' \ - '' > /tmp/central-mirror.xml - margs=(-B -q -fn -DskipTests -gs /tmp/central-mirror.xml dependency:go-offline) - [ -f .mvn/settings.xml ] && margs+=(-s .mvn/settings.xml) - mvn "${margs[@]}" | tee /tmp/mvn.log || true - if grep -q '\[ERROR\]' /tmp/mvn.log; then - echo "::warning::Some Maven dependencies could not be resolved; pom.xml scan depth may be reduced" - fi + if [ -f pom.xml ]; then # warm ~/.m2 via Google's Central mirror so trivy sees parent-managed versions + echo 'google-centralhttps://maven-central.storage-download.googleapis.com/maven2/central' > /tmp/mirror.xml + mvn -B -q -fn -DskipTests -gs /tmp/mirror.xml $([ -f .mvn/settings.xml ] && echo '-s .mvn/settings.xml') dependency:go-offline | tee /tmp/mvn.log || true + ! grep -q '\[ERROR\]' /tmp/mvn.log || echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" fi + trivy fs --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json -o /tmp/t.json . + jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' /tmp/t.json | sort -u > "trivy-${GITHUB_REPOSITORY##*/}-code-report.tsv" - # --- fs: lockfile dependencies --- - trivy fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan \ - --format json -o trivy-report.json . - jq '.Results = ((.Results // []) | map(. + {ScanSrc: "fs"}))' trivy-report.json > combined.json - - # --- image: base images from Dockerfiles --- - find . \( -name 'Dockerfile' -o -name 'Dockerfile.*' -o -name 'Dockerfile-*' \) -not -path '*/node_modules/*' -not -path './.git/*' > dfs.txt || : > dfs.txt - : > aliases.txt - : > imgs.txt - while IFS= read -r df; do - awk 'toupper($1)=="FROM" { if (toupper($3)=="AS") print $4; if (toupper($4)=="AS") print $5 }' "$df" >> aliases.txt - awk 'toupper($1)=="FROM" { i=$2; if (i ~ /^--/) i=$3; print i }' "$df" >> imgs.txt - done < dfs.txt - sort -u imgs.txt -o imgs.txt - while IFS= read -r img; do - [ -z "$img" ] && continue - [ "$img" = "scratch" ] && continue + out="trivy-${GITHUB_REPOSITORY##*/}-docker-report.tsv"; : > "$out" + find . -name 'Dockerfile*' -not -path './.git/*' -not -path '*/node_modules/*' -print0 | + xargs -0 -r awk 'toupper($1)=="FROM" {i=$2; if (i ~ /^--/) i=$3; print i; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5}' | sort -u > /tmp/from.txt + for img in $(grep -v '^~' /tmp/from.txt); do + [ "$img" = scratch ] && continue case "$img" in *'$'*) echo "::warning::Skipping base image with unresolved variable: ${img}"; continue ;; esac - grep -qxF "$img" aliases.txt && continue - echo "Scanning base image ${img}" - if trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --format json -o img.json "$img"; then - jq --arg s "$img" '.Results = ((.Results // []) | map(. + {ScanSrc: ("image " + $s)}))' img.json > img2.json - jq -s '{Results: (.[0].Results + (.[1].Results // []))}' combined.json img2.json > tmp.json && mv tmp.json combined.json - else + grep -qxF "~$img" /tmp/from.txt && continue + trivy image --no-progress --scanners vuln --severity CRITICAL --ignore-unfixed --format json -o /tmp/t.json "$img" && + jq -r --arg img "$img" '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $img] | @tsv' /tmp/t.json >> "$out" || echo "::warning::Base image ${img} could not be scanned (pull/scan error); it was NOT checked" - fi - done < imgs.txt + done + sort -u "$out" -o "$out" + + - name: Scan built image + if: inputs.scan == 'image' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.maven-token }} + GITHUB_ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + docker buildx build --platform linux/amd64 --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN --build-arg GITHUB_ACTOR="$GITHUB_ACTOR" \ + -f "${{ inputs.dockerfile }}" --output type=oci,dest=/tmp/image.tar "${{ inputs.context }}" + out="trivy-${GITHUB_REPOSITORY##*/}-image-report-${{ inputs.image-name }}.tsv" + trivy image --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --input /tmp/image.tar --format json -o /tmp/t.json + jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' /tmp/t.json | sort -u > "$out" + + - name: Upload code report + if: inputs.scan == 'code' + uses: actions/upload-artifact@v4 + with: + name: trivy-${{ github.event.repository.name }}-code-report + path: trivy-*-code-report.tsv - # --- report (deduplicated: one row per unique vulnerability, with occurrence count) --- - total=$(jq '[.Results[]?.Vulnerabilities[]?] | length' combined.json) - uniq=$(jq '[.Results[]? as $r | $r.Vulnerabilities[]? - | [.Severity, ($r.ScanSrc // "fs"), .PkgName, .VulnerabilityID]] | unique | length' combined.json) - if [ "$total" -gt 0 ]; then - echo "" - echo "=== HIGH/CRITICAL findings: ${uniq} unique (${total} occurrences across modules/images) ===" - jq -r '[.Results[]? as $r | $r.Vulnerabilities[]? - | {s: .Severity, ty: ($r.ScanSrc // "fs"), p: .PkgName, i: .InstalledVersion, - f: (.FixedVersion // "-"), id: .VulnerabilityID}] - | group_by([.s, .ty, .p, .id]) | map(.[0] + {n: length}) | sort_by(.s, .ty, .p) - | .[] | [.s, .ty, .p, .i + " -> " + .f, .id, "x\(.n)"] | @tsv' combined.json \ - | column -t -s "$(printf '\t')" - echo "" - echo "::error::Trivy found ${uniq} unique HIGH/CRITICAL vulnerabilities (${total} occurrences, fs + base images)" - exit 1 + - name: Upload docker report + if: inputs.scan == 'code' + uses: actions/upload-artifact@v4 + with: + name: trivy-${{ github.event.repository.name }}-docker-report + path: trivy-*-docker-report.tsv + + - name: Upload image report + if: inputs.scan == 'image' + uses: actions/upload-artifact@v4 + with: + name: trivy-${{ github.event.repository.name }}-image-report-${{ inputs.image-name }} + path: trivy-*-image-report-*.tsv + + - name: Evaluate + shell: bash + env: + IMAGE_NAME: ${{ inputs.image-name }} + run: | + set -euo pipefail + repo="${GITHUB_REPOSITORY##*/}" + if [ "${{ inputs.scan }}" = "code" ]; then + c="trivy-${repo}-code-report.tsv"; d="trivy-${repo}-docker-report.tsv" + [ -s "$c" ] || [ -s "$d" ] || { echo "No vulnerabilities found."; exit 0; } + cat "$c" "$d" | column -t -s "$(printf '\t')" + echo "::error::Trivy: $(cut -f1,2,5 "$c" | sort -u | wc -l | tr -d ' ') unique HIGH/CRITICAL in dependencies, $(wc -l < "$d" | tr -d ' ') CRITICAL in Dockerfile base images (see the trivy-${repo}-code-report artifact)" + else + f="trivy-${repo}-image-report-${IMAGE_NAME}.tsv" + [ -s "$f" ] || { echo "No vulnerabilities found."; exit 0; } + column -t -s "$(printf '\t')" "$f" + echo "::error::Trivy found HIGH/CRITICAL vulnerabilities in the ${IMAGE_NAME} image (report in the ${f%.tsv} artifact)" fi - echo "No HIGH/CRITICAL vulnerabilities found." + exit 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9ff7df91d..4919d19823 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,13 +39,13 @@ jobs: !github.event.pull_request.draft - trivy_scan: - name: "Trivy Scan" + trivy_code: + name: "Trivy Code Scan" runs-on: ubuntu-latest + needs: [changes] permissions: contents: read packages: read - if: github.event_name == 'pull_request' && github.event.pull_request.state == 'open' && !github.event.pull_request.draft steps: - name: Checkout uses: actions/checkout@v4 @@ -53,8 +53,10 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - - name: Trivy scan (fs + base images) + - name: Trivy scan (dependencies + Dockerfile base images) uses: ./.github/steps/trivy + with: + scan: code test: name: "Test meshcentral" From 7ddfa2d6ac9fb93db5f1f958c99f8622a3f46954 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Wed, 5 Aug 2026 14:26:35 -0400 Subject: [PATCH 5/8] ci: merge all trivy reports into one artifact per run, skip empty reports --- .github/steps/trivy/action.yml | 6 ++++++ .github/workflows/test.yml | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index f0b05da5cf..072b749606 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -56,6 +56,7 @@ runs: echo "::warning::Base image ${img} could not be scanned (pull/scan error); it was NOT checked" done sort -u "$out" -o "$out" + find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Scan built image if: inputs.scan == 'image' @@ -70,6 +71,7 @@ runs: out="trivy-${GITHUB_REPOSITORY##*/}-image-report-${{ inputs.image-name }}.tsv" trivy image --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --input /tmp/image.tar --format json -o /tmp/t.json jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' /tmp/t.json | sort -u > "$out" + find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Upload code report if: inputs.scan == 'code' @@ -77,6 +79,7 @@ runs: with: name: trivy-${{ github.event.repository.name }}-code-report path: trivy-*-code-report.tsv + if-no-files-found: ignore - name: Upload docker report if: inputs.scan == 'code' @@ -84,6 +87,7 @@ runs: with: name: trivy-${{ github.event.repository.name }}-docker-report path: trivy-*-docker-report.tsv + if-no-files-found: ignore - name: Upload image report if: inputs.scan == 'image' @@ -91,6 +95,7 @@ runs: with: name: trivy-${{ github.event.repository.name }}-image-report-${{ inputs.image-name }} path: trivy-*-image-report-*.tsv + if-no-files-found: ignore - name: Evaluate shell: bash @@ -101,6 +106,7 @@ runs: repo="${GITHUB_REPOSITORY##*/}" if [ "${{ inputs.scan }}" = "code" ]; then c="trivy-${repo}-code-report.tsv"; d="trivy-${repo}-docker-report.tsv" + [ -f "$c" ] || c=/dev/null; [ -f "$d" ] || d=/dev/null [ -s "$c" ] || [ -s "$d" ] || { echo "No vulnerabilities found."; exit 0; } cat "$c" "$d" | column -t -s "$(printf '\t')" echo "::error::Trivy: $(cut -f1,2,5 "$c" | sort -u | wc -l | tr -d ' ') unique HIGH/CRITICAL in dependencies, $(wc -l < "$d" | tr -d ' ') CRITICAL in Dockerfile base images (see the trivy-${repo}-code-report artifact)" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4919d19823..5e1c0db1a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,20 @@ jobs: with: scan: code + trivy_report: + name: "Trivy Report" + runs-on: ubuntu-latest + needs: [trivy_code] + if: always() + steps: + - name: Merge all scan reports into one artifact + continue-on-error: true + uses: actions/upload-artifact/merge@v4 + with: + name: trivy-${{ github.event.repository.name }}-report + pattern: trivy-*-report* + delete-merged: true + test: name: "Test meshcentral" needs: [changes] From 7563a74b907b7bed552674b4bca54dfcaf27575d Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Wed, 5 Aug 2026 14:46:32 -0400 Subject: [PATCH 6/8] ci: merge all trivy reports into one artifact per run, dedupe upload step --- .github/steps/trivy/action.yml | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 072b749606..e733be5dcb 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -73,28 +73,11 @@ runs: jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' /tmp/t.json | sort -u > "$out" find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - - name: Upload code report - if: inputs.scan == 'code' - uses: actions/upload-artifact@v4 - with: - name: trivy-${{ github.event.repository.name }}-code-report - path: trivy-*-code-report.tsv - if-no-files-found: ignore - - - name: Upload docker report - if: inputs.scan == 'code' - uses: actions/upload-artifact@v4 - with: - name: trivy-${{ github.event.repository.name }}-docker-report - path: trivy-*-docker-report.tsv - if-no-files-found: ignore - - - name: Upload image report - if: inputs.scan == 'image' + - name: Upload report uses: actions/upload-artifact@v4 with: - name: trivy-${{ github.event.repository.name }}-image-report-${{ inputs.image-name }} - path: trivy-*-image-report-*.tsv + name: ${{ inputs.scan == 'code' && format('trivy-{0}-code-report', github.event.repository.name) || format('trivy-{0}-image-report-{1}', github.event.repository.name, inputs.image-name) }} + path: trivy-*.tsv if-no-files-found: ignore - name: Evaluate From cfe4a2012b2104747c79696e676ed8c1a768fe9f Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Wed, 5 Aug 2026 14:50:40 -0400 Subject: [PATCH 7/8] =?UTF-8?q?ci:=20compact=20trivy=20action=20=E2=80=94?= =?UTF-8?q?=20piped=20scans,=20unified=20evaluate,=20single=20merged=20rep?= =?UTF-8?q?ort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/steps/trivy/action.yml | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index e733be5dcb..42ecccfa50 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -41,8 +41,8 @@ runs: mvn -B -q -fn -DskipTests -gs /tmp/mirror.xml $([ -f .mvn/settings.xml ] && echo '-s .mvn/settings.xml') dependency:go-offline | tee /tmp/mvn.log || true ! grep -q '\[ERROR\]' /tmp/mvn.log || echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" fi - trivy fs --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json -o /tmp/t.json . - jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' /tmp/t.json | sort -u > "trivy-${GITHUB_REPOSITORY##*/}-code-report.tsv" + trivy fs --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json . | + jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' | sort -u > "trivy-${GITHUB_REPOSITORY##*/}-code-report.tsv" out="trivy-${GITHUB_REPOSITORY##*/}-docker-report.tsv"; : > "$out" find . -name 'Dockerfile*' -not -path './.git/*' -not -path '*/node_modules/*' -print0 | @@ -51,8 +51,8 @@ runs: [ "$img" = scratch ] && continue case "$img" in *'$'*) echo "::warning::Skipping base image with unresolved variable: ${img}"; continue ;; esac grep -qxF "~$img" /tmp/from.txt && continue - trivy image --no-progress --scanners vuln --severity CRITICAL --ignore-unfixed --format json -o /tmp/t.json "$img" && - jq -r --arg img "$img" '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $img] | @tsv' /tmp/t.json >> "$out" || + trivy image --no-progress --scanners vuln --severity CRITICAL --ignore-unfixed --format json "$img" | + jq -r --arg img "$img" '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $img] | @tsv' >> "$out" || echo "::warning::Base image ${img} could not be scanned (pull/scan error); it was NOT checked" done sort -u "$out" -o "$out" @@ -69,8 +69,8 @@ runs: docker buildx build --platform linux/amd64 --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN --build-arg GITHUB_ACTOR="$GITHUB_ACTOR" \ -f "${{ inputs.dockerfile }}" --output type=oci,dest=/tmp/image.tar "${{ inputs.context }}" out="trivy-${GITHUB_REPOSITORY##*/}-image-report-${{ inputs.image-name }}.tsv" - trivy image --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --input /tmp/image.tar --format json -o /tmp/t.json - jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' /tmp/t.json | sort -u > "$out" + trivy image --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --input /tmp/image.tar --format json | + jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' | sort -u > "$out" find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Upload report @@ -82,21 +82,9 @@ runs: - name: Evaluate shell: bash - env: - IMAGE_NAME: ${{ inputs.image-name }} run: | set -euo pipefail - repo="${GITHUB_REPOSITORY##*/}" - if [ "${{ inputs.scan }}" = "code" ]; then - c="trivy-${repo}-code-report.tsv"; d="trivy-${repo}-docker-report.tsv" - [ -f "$c" ] || c=/dev/null; [ -f "$d" ] || d=/dev/null - [ -s "$c" ] || [ -s "$d" ] || { echo "No vulnerabilities found."; exit 0; } - cat "$c" "$d" | column -t -s "$(printf '\t')" - echo "::error::Trivy: $(cut -f1,2,5 "$c" | sort -u | wc -l | tr -d ' ') unique HIGH/CRITICAL in dependencies, $(wc -l < "$d" | tr -d ' ') CRITICAL in Dockerfile base images (see the trivy-${repo}-code-report artifact)" - else - f="trivy-${repo}-image-report-${IMAGE_NAME}.tsv" - [ -s "$f" ] || { echo "No vulnerabilities found."; exit 0; } - column -t -s "$(printf '\t')" "$f" - echo "::error::Trivy found HIGH/CRITICAL vulnerabilities in the ${IMAGE_NAME} image (report in the ${f%.tsv} artifact)" - fi + files=$(ls trivy-*.tsv 2>/dev/null) || { echo "No vulnerabilities found."; exit 0; } + column -t -s "$(printf '\t')" $files + echo "::error::Trivy found $(cat $files | wc -l | tr -d ' ') HIGH/CRITICAL vulnerabilities in: ${files//$'\n'/, } (full report in the run artifacts)" exit 1 From 95017b0c548bc8f2060b8c050742c5859ceeefbd Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Wed, 5 Aug 2026 15:01:48 -0400 Subject: [PATCH 8/8] ci: report unique vulnerability count in trivy error message --- .github/steps/trivy/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 42ecccfa50..2500d8ca48 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -86,5 +86,5 @@ runs: set -euo pipefail files=$(ls trivy-*.tsv 2>/dev/null) || { echo "No vulnerabilities found."; exit 0; } column -t -s "$(printf '\t')" $files - echo "::error::Trivy found $(cat $files | wc -l | tr -d ' ') HIGH/CRITICAL vulnerabilities in: ${files//$'\n'/, } (full report in the run artifacts)" + echo "::error::Trivy found $(cut -f1,2,5 $files | sort -u | wc -l | tr -d ' ') unique HIGH/CRITICAL vulnerabilities ($(cat $files | wc -l | tr -d ' ') occurrences across modules) in: ${files//$'\n'/, } (full report in the run artifacts)" exit 1