Files
OpenShell/tasks/scripts/trivy-scan.sh
Drew Newberry c1f2e7189f feat(isolation): implement the RFC 0012 sandbox architecture (#2942)
* feat(isolation): add RFC 0012 backend contract

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>

* refactor(isolation): name the interface crate explicitly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): expose trusted host gateway

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(agents): inventory the MXC driver

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add mediated DNS transport

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): tighten interface error and digest contracts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(isolation): remove unrelated driver inventory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): define capability-free launch contract

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): seal confirmed boundary state

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate confirmation for external backend implementations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): clarify mediated DNS identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): generalize loopback connector

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): unify typed network mediation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): bind launches to sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(mxc): initialize extended sandbox status

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add boundary protocol and Linux primitives

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden signals and separate process status from transport

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate remote confirmation through public contract

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate wire state and propagate snapshot failures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(isolation): import owned agent specification explicitly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(isolation): describe mediated DNS channel

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): bound mediation attach without nested retries

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): generalize loopback protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add transport-neutral session authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): separate sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden runtime boundary controls

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add terminal boundary operation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): split supervisor and sandbox runtimes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): harden boundary isolation and lifecycle ownership

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): reject private root redirects and adopt typed errors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): preserve accept thread ownership on musl

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(sandbox): isolate credential probes from filtered threads

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): return retained exec exit status to independent waiters

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bound network mediation and preserve socket authorization

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bound control admission and retire stale mediation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* ci(e2e): select migrated drivers per stack layer

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): implement loopback connector

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): authenticate the Sandbox Protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(supervisor): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): consume dedicated backend crate

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(sandbox): align topology session fixture

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): align projected bootstrap bundle

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): validate refreshed credentials before rotation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): fail closed across supervisor disconnects

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): repair rebased sandbox CI

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* build(runtime): publish separate sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(config): configure the sandbox runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): validate sandbox binary linkage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): use backend and runtime terminology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): use a scratch runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): refresh schema and dependency policy

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bind reconnects to supervisor process

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs: align runtime split operational guidance

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(security): document Kubernetes runtime RBAC

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): enforce runtime lifecycle invariants

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(compute): identify sandbox start generations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(server): restore sandbox launch sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): support authenticated runtime replacement

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): bind sandbox session successors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): retry pending sandbox successors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(vm): run the supervisor outside the guest workload

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): repair rebase integration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): use unified build toolchain

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): use sandbox runtime terminology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): own guest network bootstrap

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): expose guest init version

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): select native supervisor artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): guard guest init Linux symbols

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): scope Linux test imports

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): avoid guest interface casts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): reconcile admitted sandbox identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): share resolved sandbox identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): surface host supervisor failures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): include guest logs on supervisor exit

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): rotate and clean runtime generations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): keep shared paths in the base layer

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): isolate workloads behind the host supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use host networking for supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve host gateway alias resolution

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): use separate sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): restore startup validation after rebase

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): name the sandbox runtime directly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): narrow supervisor CA runtime storage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): close companion isolation gaps

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(docker): align mediated network expectations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(docker): exercise mediated network paths

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): attach supervisor to managed network

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): defer supervisor recovery until gateway is ready

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve workloads during session rotation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): remove unrelated configuration RFC changes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(kubernetes): add proxy-pod isolation topology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): use stable sandbox service authority

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(kubernetes): split sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): adapt proxy pods to current runtime APIs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): describe the single runtime placement

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): simplify sandbox orchestration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): validate deployment prerequisites

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): update Trivy Helm profile inventory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(kubernetes): update Trivy scan inventory count

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): reuse preloaded runtime images in e2e

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): type and clean runtime resources

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): make sandbox restarts recoverable

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): preserve supervisor egress

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): adopt isolated sandbox and supervisor containers

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): stage bootstrap archives at named volume destinations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): use host networking for supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): split sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): repair rebase integration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): name the sandbox runtime directly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): provision supervisor CA runtime storage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): address isolation review findings

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): inspect Debian supervisor provenance

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): use libpod-compatible tmpfs options

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): bind verified sandbox runtime binary

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): provide external driver data directory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): start sandbox before joining user namespace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): separate supervisor user namespace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* perf(isolation): add TCP and DNS benchmark harnesses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): align benchmark timing and supported protocols

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): report TCP benchmark metrics accurately

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): cancel failed worker startup

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): build matching local supervisor image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): make local sandbox smoke test runnable

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): wire local sandbox runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): narrow sandbox service RBAC

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): validate split runtime artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden runtime session handling

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(supervisor): add standalone network proxy role

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(rfc): remove implementation companion notes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): standardize runtime release name

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): pin renamed runtime artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): persist sandbox runtime identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(runtime): restore branch validation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): reconcile main after rebase

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(network): close unframed HTTP 1.0 responses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(isolation): preserve upstream OCSF updates

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(security): close credential and TLS replay paths

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): make sandbox refresh retries idempotent

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-09-16 00:49:14 +00:00

450 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
# `config` and `images` write full-severity reports and never fail on findings,
# then `gate` applies TRIVY_SEVERITY (default HIGH,CRITICAL).
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SOURCE_ROOT="${TRIVY_SOURCE_ROOT:-${REPO_ROOT}}"
IGNORE_FILE="${TRIVY_IGNORE_FILE:-${REPO_ROOT}/.trivyignore.yaml}"
cd "${SOURCE_ROOT}"
SEVERITY="${TRIVY_SEVERITY:-HIGH,CRITICAL}"
REPORT_DIR="${TRIVY_REPORT_DIR:-reports/trivy}"
IGNORE_UNFIXED="${TRIVY_IGNORE_UNFIXED:-true}"
PLATFORMS="${TRIVY_PLATFORMS:-linux/amd64 linux/arm64}"
# Disable cluster discovery while rendering charts offline.
PREFLIGHT_OFF=(--helm-set agentSandbox.preflight.enabled=false)
# These Dockerfiles do not produce release runtime images.
SKIP_DOCKERFILES=(
--skip-files 'deploy/docker/Dockerfile.ci'
--skip-files 'deploy/docker/Dockerfile.*-macos'
)
# Explicit OpenShell variants, including dev/E2E regression coverage.
# spire-stack belongs to the external SPIRE chart and is intentionally absent.
HELM_PROFILES=(
cert-manager credential-driver-kubernetes-secrets credential-driver-vault
gateway gateway-tls high-availability openshift-route-cert-manager spire
tls-disabled workspace-managed workspace-operator
corporate-proxy-e2e keycloak skaffold
)
# Reject ignore entries broader than one concrete basename.
validate_ignore_file() {
[ -f "${IGNORE_FILE}" ] || {
echo "Error: Trivy ignore file not found: ${IGNORE_FILE}" >&2
return 2
}
command -v yq >/dev/null || {
echo "Error: yq not on PATH; run inside 'nix develop'" >&2
return 2
}
if ! yq --output-format json '.' "${IGNORE_FILE}" |
jq -e '
(.misconfigurations // []) as $entries
| (($entries | type) == "array")
and all($entries[];
. as $entry
| (($entry.id | type) == "string")
and (($entry.paths | type) == "array")
and (($entry.paths | length) > 0)
and all($entry.paths[];
. as $path
| (($path | type) == "string")
and ($path | startswith("**/"))
and (($path | ltrimstr("**/") | length) > 0)
and (($path | ltrimstr("**/") | test("[/*?\\[\\]]")) | not)
)
)
' >/dev/null; then
echo "Error: every Trivy ignore must use at least one '**/<concrete-basename>' path" >&2
return 2
fi
}
# Convert a report whose targets already use repository-relative paths.
convert_sarif() {
local report=$1 output=$2 category=$3
trivy convert --quiet \
--severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL --exit-code 0 \
--format sarif --output "${output}" "${report}"
# `convert` can set ROOTPATH to the input JSON file. Our targets are relative
# to the checkout, not to the report file or the original scan directory.
jq --arg automation_id "trivy/${category}/" '
.runs[] |= (.automationDetails.id = $automation_id)
| del(.runs[].originalUriBaseIds)
| (.. | objects | select(has("artifactLocation")) | .artifactLocation)
|= del(.uriBaseId)
' "${output}" >"${output}.tmp"
mv "${output}.tmp" "${output}"
}
scan() {
local subcommand=$1 slug=$2 prefix=$3
shift 3
echo "==> ${slug}"
trivy "${subcommand}" --skip-version-check --quiet \
--ignorefile "${IGNORE_FILE}" \
--severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL --exit-code 0 \
--format json --output "${REPORT_DIR}/${slug}.json" "$@"
if [ -n "${prefix}" ]; then
jq --arg prefix "${prefix}" --arg profile "${slug}" '
.TrivyProfile = $profile
| (.Results[]?.Target) |= $prefix + sub("^[^:]*\\.tgz:"; "")
' "${REPORT_DIR}/${slug}.json" >"${REPORT_DIR}/${slug}.json.tmp"
mv "${REPORT_DIR}/${slug}.json.tmp" "${REPORT_DIR}/${slug}.json"
fi
}
# Scan static deployment files once, then charts with their applicable values.
scan_config() {
scan config config-static deploy/ "${SKIP_DOCKERFILES[@]}" \
--skip-dirs deploy/helm deploy
scan config config-defaults deploy/helm/ "${PREFLIGHT_OFF[@]}" deploy/helm
local values fixture
for fixture in "${HELM_PROFILES[@]}"; do
values="deploy/helm/openshell/ci/values-${fixture}.yaml"
if [ ! -f "${values}" ]; then
# A newly added profile has no baseline report yet. A missing candidate
# fixture is an error: intentional removal must also update HELM_PROFILES.
[ "${SOURCE_ROOT}" != "${REPO_ROOT}" ] && continue
echo "Error: Helm profile not found: ${values}" >&2
return 2
fi
scan config "config-fixture-${fixture}" deploy/helm/openshell/ \
"${PREFLIGHT_OFF[@]}" --helm-values "${values}" deploy/helm/openshell
done
}
# A readable label plus the full reference hash avoids registry/tag collisions.
artifact_slug() {
local digest
digest="$(printf '%s' "$1" | sha256sum)"
printf '%s-%s' "$(printf '%s' "${1##*/}" | tr -cs 'A-Za-z0-9._-' '-' | cut -c1-80)" "${digest%% *}"
}
# Trivy needs a local chart archive rather than an OCI reference.
scan_packaged_chart() {
local ref=$1
if [[ "${ref}" != *:* || "${ref##*/}" != *:* ]]; then
echo "Error: --chart-ref needs a version tag, e.g. oci://host/chart:1.2.3" >&2
exit 2
fi
local repo chart_name chart_dir="" candidate dir
repo="${ref%:*}"
chart_name="${repo##*/}"
for candidate in deploy/helm/*/; do
[ -f "${candidate}Chart.yaml" ] || continue
[ "$(sed -n 's/^name:[[:space:]]*//p' "${candidate}Chart.yaml" | head -1)" \
= "${chart_name}" ] || continue
chart_dir="${candidate}"
break
done
if [ -z "${chart_dir}" ]; then
echo "Error: no chart under deploy/helm declares name '${chart_name}'" >&2
exit 2
fi
dir="$(mktemp -d)"
trap 'rm -rf "${dir}"' RETURN
helm pull "${repo}" --version "${ref##*:}" --destination "${dir}"
scan config "config-packaged-$(artifact_slug "${ref}")" "${chart_dir}" \
"${PREFLIGHT_OFF[@]}" "$(find "${dir}" -name '*.tgz' -print -quit)"
}
scan_images() {
local extra=()
[ "${IGNORE_UNFIXED}" = "true" ] && extra+=(--ignore-unfixed)
local image platform slug
for image in "$@"; do
for platform in ${PLATFORMS}; do
slug="image-$(artifact_slug "${image}")-${platform//\//-}"
scan image "${slug}" "" --platform "${platform}" --scanners vuln \
"${extra[@]}" "${image}"
done
done
}
gate() {
local report result findings=0
if [ -z "$(find "${REPORT_DIR}" -maxdepth 1 -name '*.json' -print -quit)" ]; then
echo "Error: no reports in ${REPORT_DIR}; run 'config' or 'images' first" >&2
exit 2
fi
local reports=()
if [ -f "${REPORT_DIR}/config-defaults.json" ]; then
consolidate_config
reports+=("${REPORT_DIR}/consolidated/config.json")
fi
for report in "${REPORT_DIR}"/image-*.json "${REPORT_DIR}"/config-packaged-*.json; do
[ -f "${report}" ] && reports+=("${report}")
done
[ "${#reports[@]}" -gt 0 ] || { echo "Error: no complete scan reports" >&2; return 2; }
for report in "${reports[@]}"; do
set +e
trivy convert --quiet --exit-code 10 --severity "${SEVERITY}" \
--format table "${report}"
result=$?
set -e
case "${result}" in
0) ;;
10) findings=1 ;;
*)
echo "Error: Trivy could not evaluate ${report} (exit ${result})" >&2
return "${result}"
;;
esac
done
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### Trivy (gate: \`${SEVERITY}\`)"
echo '```'
for report in "${reports[@]}"; do
trivy convert --quiet --severity "${SEVERITY}" --format table "${report}"
done
echo '```'
} >>"${GITHUB_STEP_SUMMARY}"
fi
[ "${findings}" -eq 0 ] || return 10
}
consolidate_config() {
local report
local reports=("${REPORT_DIR}/config-static.json" "${REPORT_DIR}/config-defaults.json")
for report in "${REPORT_DIR}"/config-fixture-*.json; do
[ -f "${report}" ] && reports+=("${report}")
done
mkdir -p "${REPORT_DIR}/consolidated"
jq -s -f "${REPO_ROOT}/tasks/scripts/trivy-config-report.jq" \
"${reports[@]}" >"${REPORT_DIR}/consolidated/config.json"
}
# Keep detailed reports for the differential gate, but publish one deduplicated
# configuration analysis. Images and packaged charts retain separate identities.
prepare_sarif() {
local staging report slug batch=0 count=0
staging="${REPORT_DIR}/code-scanning"
if [ -e "${staging}" ]; then
echo "Error: ${staging} already exists; use a fresh report directory" >&2
return 2
fi
mkdir -p "${staging}/uploads/0"
consolidate_config
convert_sarif "${REPORT_DIR}/consolidated/config.json" "${staging}/uploads/0/config.sarif" config
count=1
for report in "${REPORT_DIR}"/image-*.json "${REPORT_DIR}"/config-packaged-*.json; do
[ -f "${report}" ] || continue
# upload-sarif combines a directory into one file; GitHub accepts at most
# 20 runs per file. Every report produced here contains exactly one run.
if [ "${count}" -eq 20 ]; then
batch=$((batch + 1))
count=0
mkdir -p "${staging}/uploads/${batch}"
fi
slug="$(basename "${report}" .json)"
convert_sarif "${report}" "${staging}/uploads/${batch}/${slug}.sarif" "${slug}"
count=$((count + 1))
done
if [ -n "${GITHUB_OUTPUT:-}" ]; then
printf 'batches=%s\n' "$(jq -cn --argjson last "${batch}" '[range(0; $last + 1) | tostring]')" \
>>"${GITHUB_OUTPUT}"
fi
}
collect_config_findings() {
# Preserve report/profile identity while counting repeated findings.
# With no reports, the unmatched glob makes jq fail on the missing input.
jq -n --arg severities "${SEVERITY}" '
[inputs
| (input_filename | split("/")[-1] | rtrimstr(".json")) as $profile
| if .SchemaVersion != 2
or ((.ArtifactName | type) != "string")
or ((.ArtifactType | type) != "string")
or (.Results != null and ((.Results | type) != "array"))
then
error("invalid Trivy JSON report: " + $profile)
else
{
profile: $profile,
findings: ([
.Results[]? as $result
| $result.Misconfigurations[]?
| .Severity as $severity
| select(($severities | split(",") | index($severity)) != null)
| ([
.ID,
$result.Target,
(.Namespace // ""),
(.Message // ""),
(.CauseMetadata.Provider // ""),
(.CauseMetadata.Service // ""),
(.CauseMetadata.Resource // "")
] | @json) as $semantic_key
| {
key: ([$profile, $semantic_key] | @json),
semantic_key: $semantic_key,
profile: $profile,
severity: .Severity,
id: .ID,
target: $result.Target,
title: .Title
}
]
| group_by(.key)
| map(.[0] + { count: length }))
}
end
] | {
profiles: map(.profile),
findings: (map(.findings) | add // [])
}
' "$1"/*.json
}
# Compare semantic identities and occurrence counts, excluding line numbers.
gate_config_diff() (
set -euo pipefail
local baseline_dir=$1 candidate_dir=$2
local inventory_dir baseline candidate new_findings finding_count
inventory_dir="$(mktemp -d)"
trap 'rm -rf "${inventory_dir}"' EXIT
baseline="${inventory_dir}/baseline.json"
candidate="${inventory_dir}/candidate.json"
new_findings="${inventory_dir}/new.json"
collect_config_findings "${baseline_dir}" >"${baseline}"
collect_config_findings "${candidate_dir}" >"${candidate}"
jq --slurpfile baseline "${baseline}" '
($baseline[0].findings | map({ (.key): .count }) | add // {}) as $by_profile
| ($baseline[0].profiles) as $known_profiles
| ($baseline[0].findings
| group_by(.semantic_key)
| map({
key: .[0].semantic_key,
value: (map(.count) | max)
})
| from_entries) as $across_profiles
| [
.findings[]
| . as $finding
| (if ($known_profiles | index($finding.profile)) != null
then (($by_profile[$finding.key]) // 0)
else (($across_profiles[$finding.semantic_key]) // 0)
end) as $before
| select(.count > $before)
| . + { baseline_count: $before, new_count: (.count - $before) }
]
' "${candidate}" >"${new_findings}"
finding_count="$(jq '[.[].new_count] | add // 0' "${new_findings}")"
if [ "${finding_count}" -eq 0 ]; then
echo "No new configuration findings at ${SEVERITY}."
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
echo "No new Trivy configuration findings at \`${SEVERITY}\`." \
>>"${GITHUB_STEP_SUMMARY}"
fi
exit 0
fi
echo "::error::Trivy reported ${finding_count} new configuration finding(s) at ${SEVERITY}."
jq -r '.[]
| "::error::[\(.severity)] \(.id) in \(.profile) (\(.target)): \(.title)"
+ " (\(.new_count) new, \(.baseline_count) in baseline)"' \
"${new_findings}"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### New Trivy configuration findings"
echo
jq -r '.[]
| "- **\(.severity)** `\(.id)` in `\(.profile)`"
+ " (`\(.target)`): \(.title)"
+ " (\(.new_count) new, \(.baseline_count) in baseline)"' \
"${new_findings}"
} >>"${GITHUB_STEP_SUMMARY}"
fi
exit 10
)
require_trivy() {
command -v trivy >/dev/null || {
echo "Error: trivy not on PATH; run inside 'nix develop'" >&2
exit 2
}
}
case "${1:-}" in
config)
shift
require_trivy
validate_ignore_file
mkdir -p "${REPORT_DIR}"
scan_config
# Reject unparsed arguments so requested charts cannot be silently skipped.
while [ "${1:-}" = "--chart-ref" ]; do
[ -n "${2:-}" ] || { echo "Error: --chart-ref needs a value" >&2; exit 2; }
scan_packaged_chart "$2"
shift 2
done
[ $# -eq 0 ] || { echo "Error: unexpected argument '$1' after config" >&2; exit 2; }
;;
images)
shift
[ $# -gt 0 ] || { echo "Error: images needs at least one reference" >&2; exit 2; }
require_trivy
validate_ignore_file
mkdir -p "${REPORT_DIR}"
scan_images "$@"
;;
gate)
require_trivy
gate
;;
prepare-sarif)
require_trivy
prepare_sarif
;;
gate-config-diff)
shift
[ $# -eq 2 ] || {
echo "Error: gate-config-diff needs baseline and candidate report directories" >&2
exit 2
}
gate_config_diff "$1" "$2"
;;
validate-ignore)
validate_ignore_file
;;
*)
cat >&2 <<'USAGE'
Usage:
trivy-scan.sh config [--chart-ref <oci-ref>]...
trivy-scan.sh images <image-ref> [<image-ref>...]
trivy-scan.sh gate
trivy-scan.sh prepare-sarif
trivy-scan.sh gate-config-diff <baseline-reports> <candidate-reports>
trivy-scan.sh validate-ignore
USAGE
exit 2
;;
esac