# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 name: Trivy Scan # Manual or reusable scan of deployment configuration and supplied OCI # artifacts. Findings are informational by default; scanner errors remain fatal. on: workflow_call: inputs: candidate_ref: description: Optional pre-release tag to scan instead of the triggering revision default: "" type: string images: description: Newline-separated image references to scan type: string default: "" charts: description: Newline-separated packaged Helm chart OCI references type: string default: "" severity: description: Severities that fail the workflow type: string default: HIGH,CRITICAL ignore-unfixed: description: Ignore image vulnerabilities with no upstream fix type: boolean default: true fail-on-findings: description: Fail the run on findings instead of warning type: boolean default: false upload-sarif: description: Upload results to GitHub Code Scanning type: boolean default: true secrets: CACHIX_AUTH_TOKEN: description: Token used to write Nix build outputs to Cachix workflow_dispatch: inputs: images: description: Newline-separated image references to scan type: string default: "" charts: description: Newline-separated packaged Helm chart OCI references to scan type: string default: "" severity: description: Severities that fail the workflow type: string default: HIGH,CRITICAL ignore-unfixed: description: Ignore image vulnerabilities with no upstream fix type: boolean default: true fail-on-findings: description: Fail the run on findings instead of warning type: boolean default: false upload-sarif: description: Upload results to GitHub Code Scanning type: boolean default: true permissions: contents: read defaults: run: shell: nix develop --command bash -euo pipefail {0} env: TRIVY_SEVERITY: ${{ inputs.severity || 'HIGH,CRITICAL' }} TRIVY_REPORT_DIR: reports/trivy jobs: scan: name: ${{ inputs.fail-on-findings && 'OpenShell / Trivy' || 'OpenShell / Trivy (informational)' }} runs-on: ubuntu-latest timeout-minutes: 60 outputs: sarif-batches: ${{ steps.sarif.outputs.batches }} artifact-id: ${{ steps.reports.outputs.artifact-id }} permissions: contents: read packages: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.candidate_ref && format('refs/tags/{0}', inputs.candidate_ref) || '' }} persist-credentials: false - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Set up Nix uses: ./.github/actions/setup-nix with: cachix-auth-token: ${{ github.event_name != 'pull_request' && secrets.CACHIX_AUTH_TOKEN || '' }} - name: Test scan reporting run: tasks/scripts/trivy-scan-test.sh - name: Scan configuration id: config env: CHARTS: ${{ inputs.charts }} run: | args=() while IFS= read -r ref; do [ -n "$ref" ] || continue args+=(--chart-ref "$ref") done <<<"$CHARTS" tasks/scripts/trivy-scan.sh config "${args[@]}" - name: Scan images id: images if: ${{ !cancelled() }} env: IMAGES: ${{ inputs.images }} TRIVY_IGNORE_UNFIXED: ${{ inputs.ignore-unfixed }} run: | refs=() while IFS= read -r ref; do [ -n "$ref" ] || continue refs+=("$ref") done <<<"$IMAGES" if [ "${#refs[@]}" -gt 0 ]; then tasks/scripts/trivy-scan.sh images "${refs[@]}" fi - name: Prepare consolidated SARIF uploads id: sarif if: >- ${{ !cancelled() && steps.config.conclusion == 'success' && steps.images.conclusion == 'success' && inputs.upload-sarif }} run: tasks/scripts/trivy-scan.sh prepare-sarif - name: Upload reports id: reports if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: trivy-${{ github.run_id }} path: reports/trivy if-no-files-found: ignore retention-days: 14 - name: Report findings if: >- ${{ !cancelled() && steps.config.conclusion == 'success' && steps.images.conclusion == 'success' }} env: FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }} run: | set +e tasks/scripts/trivy-scan.sh gate status=$? set -e case "$status" in 0) echo "No findings at ${TRIVY_SEVERITY}." ;; 10) if [ "$FAIL_ON_FINDINGS" = "true" ]; then echo "::error::Trivy findings at ${TRIVY_SEVERITY}." exit 1 fi echo "::warning::Trivy findings at ${TRIVY_SEVERITY}; this check is informational." ;; *) echo "::error::Trivy could not evaluate the reports (exit $status)."; exit "$status" ;; esac upload-sarif: name: Publish Trivy SARIF (${{ matrix.batch }}) needs: scan # Findings may fail the scan job after reports are prepared. Publish those # complete results too, but never publish an incomplete/failed scan. if: >- ${{ !cancelled() && inputs.upload-sarif && needs.scan.outputs.sarif-batches != '' && needs.scan.outputs.artifact-id != '' }} runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read security-events: write strategy: fail-fast: false matrix: batch: ${{ fromJSON(needs.scan.outputs.sarif-batches) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 id: checkout with: ref: ${{ inputs.candidate_ref && format('refs/tags/{0}', inputs.candidate_ref) || '' }} persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.scan.outputs.artifact-id }} merge-multiple: true path: reports/trivy # One configuration run, plus one run per image/platform or packaged # chart. Each batch holds at most GitHub's limit of 20 SARIF runs. - uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: reports/trivy/code-scanning/uploads/${{ matrix.batch }} ref: ${{ inputs.candidate_ref && format('refs/tags/{0}', inputs.candidate_ref) || '' }} sha: ${{ inputs.candidate_ref && steps.checkout.outputs.commit || '' }}