Skip to content

Commit 3693b32

Browse files
authored
ci(trivy): add artifact and PR configuration scans (#3185)
* ci(trivy): add artifact and PR configuration scans Signed-off-by: Adrien Langou <alangou@nvidia.com> * fix(ci): harden Trivy gate detection and finding diff Signed-off-by: Adrien Langou <alangou@nvidia.com> * feat(ci): scan released artifacts in release pipelines Signed-off-by: Adrien Langou <alangou@nvidia.com> * fix(ci): harden and simplify Trivy scans Signed-off-by: Adrien Langou <alangou@nvidia.com> * fix(ci): consolidate Trivy reports and prevent collisions Signed-off-by: Adrien Langou <alangou@nvidia.com> --------- Signed-off-by: Adrien Langou <alangou@nvidia.com>
1 parent 6e6b3c8 commit 3693b32

11 files changed

Lines changed: 1279 additions & 4 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
name: Trivy Changes
5+
6+
on:
7+
pull_request:
8+
merge_group:
9+
types: [checks_requested]
10+
workflow_dispatch:
11+
inputs:
12+
base_sha:
13+
description: Base commit SHA to compare
14+
required: true
15+
type: string
16+
head_sha:
17+
description: Candidate commit SHA to compare
18+
required: true
19+
type: string
20+
21+
permissions:
22+
contents: read
23+
24+
concurrency:
25+
group: ${{ github.workflow }}-${{ github.ref }}
26+
cancel-in-progress: true
27+
28+
jobs:
29+
changes:
30+
name: Detect deployment configuration changes
31+
runs-on: ubuntu-latest
32+
permissions:
33+
contents: read
34+
pull-requests: read
35+
outputs:
36+
should_run: ${{ steps.default.outputs.should_run || steps.changed.outputs.any_modified }}
37+
steps:
38+
- id: default
39+
if: github.event_name != 'pull_request'
40+
run: echo "should_run=true" >> "$GITHUB_OUTPUT"
41+
42+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43+
if: github.event_name == 'pull_request'
44+
with:
45+
persist-credentials: false
46+
47+
- id: changed
48+
if: github.event_name == 'pull_request'
49+
uses: tj-actions/changed-files@aa08304bd477b800d468db44fe10f6c61f7f7b11 # v42.1.0
50+
with:
51+
# `any_modified` covers deletions, which `any_changed` omits, and a
52+
# failed diff has to fail the job: both otherwise report no relevant
53+
# change, and removing the scanner or a value fixture would skip the
54+
# scan behind a green status.
55+
fail_on_initial_diff_error: true
56+
files: |
57+
deploy/docker/**
58+
deploy/helm/**
59+
deploy/kube/**
60+
.trivyignore.yaml
61+
flake.nix
62+
flake.lock
63+
tasks/scripts/trivy-scan.sh
64+
tasks/scripts/trivy-scan-test.sh
65+
tasks/scripts/trivy-config-report.jq
66+
.github/workflows/trivy-scan.yml
67+
.github/workflows/trivy-changes.yml
68+
69+
scan:
70+
name: Scan changed deployment configuration
71+
needs: changes
72+
if: needs.changes.outputs.should_run == 'true'
73+
runs-on: ubuntu-latest
74+
timeout-minutes: 30
75+
env:
76+
BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || inputs.base_sha }}
77+
HEAD_REF: ${{ inputs.head_sha || github.sha }}
78+
defaults:
79+
run:
80+
shell: nix develop --command bash -euo pipefail {0}
81+
steps:
82+
- name: Check out candidate
83+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
84+
with:
85+
ref: ${{ env.HEAD_REF }}
86+
persist-credentials: false
87+
88+
- name: Check out baseline
89+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
90+
with:
91+
ref: ${{ env.BASE_REF }}
92+
path: .trivy-base
93+
persist-credentials: false
94+
95+
- name: Set up Nix
96+
uses: ./.github/actions/setup-nix
97+
98+
- name: Test report comparison
99+
run: tasks/scripts/trivy-scan-test.sh
100+
101+
- name: Validate candidate ignore policy
102+
run: tasks/scripts/trivy-scan.sh validate-ignore
103+
104+
# Ignore-policy changes take effect only after merge. Applying the
105+
# baseline policy to both scans prevents a candidate from exempting a new
106+
# finding in the same change that introduces it.
107+
- name: Prepare baseline ignore policy
108+
run: |
109+
if [ -f .trivy-base/.trivyignore.yaml ]; then
110+
cp .trivy-base/.trivyignore.yaml "$RUNNER_TEMP/trivy-baseline-ignore.yaml"
111+
else
112+
printf 'misconfigurations: []\n' >"$RUNNER_TEMP/trivy-baseline-ignore.yaml"
113+
fi
114+
115+
- name: Scan baseline
116+
env:
117+
TRIVY_SOURCE_ROOT: ${{ github.workspace }}/.trivy-base
118+
TRIVY_IGNORE_FILE: ${{ runner.temp }}/trivy-baseline-ignore.yaml
119+
TRIVY_REPORT_DIR: ${{ runner.temp }}/trivy-base
120+
run: |
121+
mkdir -p "$TRIVY_REPORT_DIR"
122+
"$GITHUB_WORKSPACE/tasks/scripts/trivy-scan.sh" config
123+
124+
- name: Scan candidate
125+
env:
126+
TRIVY_IGNORE_FILE: ${{ runner.temp }}/trivy-baseline-ignore.yaml
127+
TRIVY_REPORT_DIR: ${{ runner.temp }}/trivy-head
128+
run: |
129+
mkdir -p "$TRIVY_REPORT_DIR"
130+
tasks/scripts/trivy-scan.sh config
131+
132+
- name: Reject new high or critical findings
133+
run: |
134+
tasks/scripts/trivy-scan.sh gate-config-diff \
135+
"$RUNNER_TEMP/trivy-base" "$RUNNER_TEMP/trivy-head"
136+
137+
- name: Upload reports
138+
if: always()
139+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
140+
with:
141+
name: trivy-changes-${{ github.run_id }}
142+
path: |
143+
${{ runner.temp }}/trivy-base
144+
${{ runner.temp }}/trivy-head
145+
if-no-files-found: ignore
146+
retention-days: 14
147+
148+
result:
149+
name: OpenShell / Trivy Changes
150+
needs: [changes, scan]
151+
if: always()
152+
runs-on: ubuntu-latest
153+
steps:
154+
- name: Check scan result
155+
env:
156+
CHANGES_RESULT: ${{ needs.changes.result }}
157+
SHOULD_RUN: ${{ needs.changes.outputs.should_run }}
158+
SCAN_RESULT: ${{ needs.scan.result }}
159+
run: |
160+
set -euo pipefail
161+
if [ "$CHANGES_RESULT" != "success" ]; then
162+
echo "::error::Change detection concluded $CHANGES_RESULT."
163+
exit 1
164+
fi
165+
if [ "$SHOULD_RUN" = "true" ] && [ "$SCAN_RESULT" != "success" ]; then
166+
echo "::error::Trivy scan concluded $SCAN_RESULT."
167+
exit 1
168+
fi
169+
if [ "$SHOULD_RUN" != "true" ]; then
170+
echo "No Helm or Dockerfile changes to scan."
171+
fi

‎.github/workflows/trivy-scan.yml‎

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
name: Trivy Scan
5+
6+
# Manual or reusable scan of deployment configuration and supplied OCI
7+
# artifacts. Findings are informational by default; scanner errors remain fatal.
8+
9+
on:
10+
workflow_call:
11+
inputs:
12+
images:
13+
description: Newline-separated image references to scan
14+
type: string
15+
default: ""
16+
charts:
17+
description: Newline-separated packaged Helm chart OCI references
18+
type: string
19+
default: ""
20+
severity:
21+
description: Severities that fail the workflow
22+
type: string
23+
default: HIGH,CRITICAL
24+
ignore-unfixed:
25+
description: Ignore image vulnerabilities with no upstream fix
26+
type: boolean
27+
default: true
28+
fail-on-findings:
29+
description: Fail the run on findings instead of warning
30+
type: boolean
31+
default: false
32+
upload-sarif:
33+
description: Upload results to GitHub Code Scanning
34+
type: boolean
35+
default: true
36+
secrets:
37+
CACHIX_AUTH_TOKEN:
38+
description: Token used to write Nix build outputs to Cachix
39+
40+
workflow_dispatch:
41+
inputs:
42+
images:
43+
description: Newline-separated image references to scan
44+
type: string
45+
default: ""
46+
charts:
47+
description: Newline-separated packaged Helm chart OCI references to scan
48+
type: string
49+
default: ""
50+
severity:
51+
description: Severities that fail the workflow
52+
type: string
53+
default: HIGH,CRITICAL
54+
ignore-unfixed:
55+
description: Ignore image vulnerabilities with no upstream fix
56+
type: boolean
57+
default: true
58+
fail-on-findings:
59+
description: Fail the run on findings instead of warning
60+
type: boolean
61+
default: false
62+
upload-sarif:
63+
description: Upload results to GitHub Code Scanning
64+
type: boolean
65+
default: true
66+
67+
permissions:
68+
contents: read
69+
70+
defaults:
71+
run:
72+
shell: nix develop --command bash -euo pipefail {0}
73+
74+
env:
75+
TRIVY_SEVERITY: ${{ inputs.severity || 'HIGH,CRITICAL' }}
76+
TRIVY_REPORT_DIR: reports/trivy
77+
78+
jobs:
79+
scan:
80+
name: OpenShell / Trivy (informational)
81+
runs-on: ubuntu-latest
82+
timeout-minutes: 60
83+
outputs:
84+
sarif-batches: ${{ steps.sarif.outputs.batches }}
85+
artifact-id: ${{ steps.reports.outputs.artifact-id }}
86+
permissions:
87+
contents: read
88+
packages: read
89+
steps:
90+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
91+
with:
92+
persist-credentials: false
93+
94+
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
95+
with:
96+
registry: ghcr.io
97+
username: ${{ github.actor }}
98+
password: ${{ github.token }}
99+
100+
- name: Set up Nix
101+
uses: ./.github/actions/setup-nix
102+
with:
103+
cachix-auth-token: ${{ github.event_name != 'pull_request' && secrets.CACHIX_AUTH_TOKEN || '' }}
104+
105+
- name: Test scan reporting
106+
run: tasks/scripts/trivy-scan-test.sh
107+
108+
- name: Scan configuration
109+
id: config
110+
env:
111+
CHARTS: ${{ inputs.charts }}
112+
run: |
113+
args=()
114+
while IFS= read -r ref; do
115+
[ -n "$ref" ] || continue
116+
args+=(--chart-ref "$ref")
117+
done <<<"$CHARTS"
118+
tasks/scripts/trivy-scan.sh config "${args[@]}"
119+
120+
- name: Scan images
121+
id: images
122+
if: ${{ !cancelled() }}
123+
env:
124+
IMAGES: ${{ inputs.images }}
125+
TRIVY_IGNORE_UNFIXED: ${{ inputs.ignore-unfixed }}
126+
run: |
127+
refs=()
128+
while IFS= read -r ref; do
129+
[ -n "$ref" ] || continue
130+
refs+=("$ref")
131+
done <<<"$IMAGES"
132+
if [ "${#refs[@]}" -gt 0 ]; then
133+
tasks/scripts/trivy-scan.sh images "${refs[@]}"
134+
fi
135+
136+
- name: Prepare consolidated SARIF uploads
137+
id: sarif
138+
if: >-
139+
${{
140+
!cancelled()
141+
&& steps.config.conclusion == 'success'
142+
&& steps.images.conclusion == 'success'
143+
&& inputs.upload-sarif
144+
}}
145+
run: tasks/scripts/trivy-scan.sh prepare-sarif
146+
147+
- name: Upload reports
148+
id: reports
149+
if: always()
150+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
151+
with:
152+
name: trivy-${{ github.run_id }}
153+
path: reports/trivy
154+
if-no-files-found: ignore
155+
retention-days: 14
156+
157+
- name: Report findings
158+
if: >-
159+
${{
160+
!cancelled()
161+
&& steps.config.conclusion == 'success'
162+
&& steps.images.conclusion == 'success'
163+
}}
164+
env:
165+
FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }}
166+
run: |
167+
set +e
168+
tasks/scripts/trivy-scan.sh gate
169+
status=$?
170+
set -e
171+
case "$status" in
172+
0) echo "No findings at ${TRIVY_SEVERITY}." ;;
173+
10)
174+
if [ "$FAIL_ON_FINDINGS" = "true" ]; then
175+
echo "::error::Trivy findings at ${TRIVY_SEVERITY}."
176+
exit 1
177+
fi
178+
echo "::warning::Trivy findings at ${TRIVY_SEVERITY}; this check is informational."
179+
;;
180+
*) echo "::error::Trivy could not evaluate the reports (exit $status)."; exit "$status" ;;
181+
esac
182+
183+
upload-sarif:
184+
name: Publish Trivy SARIF (${{ matrix.batch }})
185+
needs: scan
186+
# Findings may fail the scan job after reports are prepared. Publish those
187+
# complete results too, but never publish an incomplete/failed scan.
188+
if: >-
189+
${{
190+
!cancelled()
191+
&& inputs.upload-sarif
192+
&& needs.scan.outputs.sarif-batches != ''
193+
&& needs.scan.outputs.artifact-id != ''
194+
}}
195+
runs-on: ubuntu-latest
196+
timeout-minutes: 10
197+
permissions:
198+
contents: read
199+
security-events: write
200+
strategy:
201+
fail-fast: false
202+
matrix:
203+
batch: ${{ fromJSON(needs.scan.outputs.sarif-batches) }}
204+
steps:
205+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
206+
with:
207+
persist-credentials: false
208+
209+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
210+
with:
211+
artifact-ids: ${{ needs.scan.outputs.artifact-id }}
212+
merge-multiple: true
213+
path: reports/trivy
214+
215+
# One configuration run, plus one run per image/platform or packaged
216+
# chart. Each batch holds at most GitHub's limit of 20 SARIF runs.
217+
- uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
218+
with:
219+
sarif_file: reports/trivy/code-scanning/uploads/${{ matrix.batch }}

0 commit comments

Comments
 (0)