Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions .github/workflows/trivy-changes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

name: Trivy Changes

on:
pull_request:
merge_group:
types: [checks_requested]
workflow_dispatch:
inputs:
base_sha:
description: Base commit SHA to compare
required: true
type: string
head_sha:
description: Candidate commit SHA to compare
required: true
type: string

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
changes:
name: Detect deployment configuration changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
should_run: ${{ steps.default.outputs.should_run || steps.changed.outputs.any_modified }}
steps:
- id: default
if: github.event_name != 'pull_request'
run: echo "should_run=true" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: github.event_name == 'pull_request'
with:
persist-credentials: false

- id: changed
if: github.event_name == 'pull_request'
uses: tj-actions/changed-files@aa08304bd477b800d468db44fe10f6c61f7f7b11 # v42.1.0
with:
# `any_modified` covers deletions, which `any_changed` omits, and a
# failed diff has to fail the job: both otherwise report no relevant
# change, and removing the scanner or a value fixture would skip the
# scan behind a green status.
fail_on_initial_diff_error: true
files: |
deploy/docker/**
deploy/helm/**
deploy/kube/**
.trivyignore.yaml
flake.nix
flake.lock
tasks/scripts/trivy-scan.sh
tasks/scripts/trivy-scan-test.sh
tasks/scripts/trivy-config-report.jq
.github/workflows/trivy-scan.yml
.github/workflows/trivy-changes.yml

scan:
name: Scan changed deployment configuration
needs: changes
if: needs.changes.outputs.should_run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
env:
BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || inputs.base_sha }}
HEAD_REF: ${{ inputs.head_sha || github.sha }}
defaults:
run:
shell: nix develop --command bash -euo pipefail {0}
steps:
- name: Check out candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.HEAD_REF }}
persist-credentials: false

- name: Check out baseline
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.BASE_REF }}
path: .trivy-base
persist-credentials: false

- name: Set up Nix
uses: ./.github/actions/setup-nix

- name: Test report comparison
run: tasks/scripts/trivy-scan-test.sh

- name: Validate candidate ignore policy
run: tasks/scripts/trivy-scan.sh validate-ignore

# Ignore-policy changes take effect only after merge. Applying the
# baseline policy to both scans prevents a candidate from exempting a new
# finding in the same change that introduces it.
- name: Prepare baseline ignore policy
run: |
if [ -f .trivy-base/.trivyignore.yaml ]; then
cp .trivy-base/.trivyignore.yaml "$RUNNER_TEMP/trivy-baseline-ignore.yaml"
else
printf 'misconfigurations: []\n' >"$RUNNER_TEMP/trivy-baseline-ignore.yaml"
fi

- name: Scan baseline
env:
TRIVY_SOURCE_ROOT: ${{ github.workspace }}/.trivy-base
TRIVY_IGNORE_FILE: ${{ runner.temp }}/trivy-baseline-ignore.yaml
TRIVY_REPORT_DIR: ${{ runner.temp }}/trivy-base
run: |
mkdir -p "$TRIVY_REPORT_DIR"
"$GITHUB_WORKSPACE/tasks/scripts/trivy-scan.sh" config

- name: Scan candidate
env:
TRIVY_IGNORE_FILE: ${{ runner.temp }}/trivy-baseline-ignore.yaml
TRIVY_REPORT_DIR: ${{ runner.temp }}/trivy-head
run: |
mkdir -p "$TRIVY_REPORT_DIR"
tasks/scripts/trivy-scan.sh config

- name: Reject new high or critical findings
run: |
tasks/scripts/trivy-scan.sh gate-config-diff \
"$RUNNER_TEMP/trivy-base" "$RUNNER_TEMP/trivy-head"

- name: Upload reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: trivy-changes-${{ github.run_id }}
path: |
${{ runner.temp }}/trivy-base
${{ runner.temp }}/trivy-head
if-no-files-found: ignore
retention-days: 14

result:
name: OpenShell / Trivy Changes
needs: [changes, scan]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check scan result
env:
CHANGES_RESULT: ${{ needs.changes.result }}
SHOULD_RUN: ${{ needs.changes.outputs.should_run }}
SCAN_RESULT: ${{ needs.scan.result }}
run: |
set -euo pipefail
if [ "$CHANGES_RESULT" != "success" ]; then
echo "::error::Change detection concluded $CHANGES_RESULT."
exit 1
fi
if [ "$SHOULD_RUN" = "true" ] && [ "$SCAN_RESULT" != "success" ]; then
echo "::error::Trivy scan concluded $SCAN_RESULT."
exit 1
fi
if [ "$SHOULD_RUN" != "true" ]; then
echo "No Helm or Dockerfile changes to scan."
fi
219 changes: 219 additions & 0 deletions .github/workflows/trivy-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# 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:
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: 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:
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
with:
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 }}
Loading
Loading