Skip to content
Open
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
169 changes: 169 additions & 0 deletions .github/workflows/trivy-changes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# 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
.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
176 changes: 176 additions & 0 deletions .github/workflows/trivy-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# 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
permissions:
contents: read
packages: read
security-events: write
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: 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: Upload SARIF to Code Scanning
if: >-
${{
!cancelled()
&& steps.config.conclusion == 'success'
&& steps.images.conclusion == 'success'
&& inputs.upload-sarif
}}
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
sarif_file: reports/trivy

- name: Upload 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ pip-delete-this-directory.txt
coverage.out
coverage/
htmlcov/

# Trivy scan reports (tasks/scripts/trivy-scan-*.sh)
/reports/
.tox/
.nox/
.coverage
Expand Down
34 changes: 34 additions & 0 deletions .trivyignore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# False positives only; unimplemented hardening and accepted risks stay visible.
# The script passes this YAML file explicitly and requires each exception to use
# `**/<concrete-basename>`, which matches source and packaged-chart paths.
# Trivy cannot scope Helm exceptions to one occurrence, so keep IDs and paths
# narrow.

misconfigurations:
# The namespace comes from `helm install -n`, not the rendered workload.
- id: KSV-0110
paths:
- "**/statefulset.yaml"
- "**/deployment.yaml"
statement: >-
An artifact of rendering the chart outside a cluster. The namespace is
supplied at install time.

# The ConfigMap stores an external Secret key name, not a credential.
- id: KSV-01010
paths:
- "**/gateway-config.yaml"
statement: >-
The ConfigMap holds the name of a key in an external Secret, not a
credential.

# Trivy cannot add this project's GHCR namespace to its trusted registries.
- id: KSV-0125
paths:
- "**/statefulset.yaml"
- "**/deployment.yaml"
statement: >-
Images come from ghcr.io/nvidia/openshell, this project's own registry.
Loading
Loading