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
39 changes: 38 additions & 1 deletion .agents/skills/sbom/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,52 @@ Generate CycloneDX SBOMs, resolve missing licenses, and export to CSV for compli

## Overview

The OpenShell SBOM tooling produces CycloneDX JSON SBOMs using Syft, resolves missing or hash-based licenses by querying public registries (crates.io, npm, PyPI), and exports the results to CSV for stakeholder review.
The OpenShell SBOM tooling produces source-tree CycloneDX JSON SBOMs using Syft, resolves missing or hash-based licenses by querying public registries (crates.io, npm, PyPI), and exports the results to CSV for stakeholder review.

SBOMs are **release artifacts only** -- they are generated on demand and not committed to the repository. Output lands in `deploy/sbom/output/` (gitignored).

Pushed gateway and supervisor images carry an SPDX SBOM and minimal SLSA provenance as OCI attestations. Branch E2E, Release Dev, and Release Tag image binaries embed cargo-auditable metadata, so their image SBOMs include linked Rust crates.

## Prerequisites

- `mise install` has been run (installs Syft and other tools)
- The repository is checked out at the root

## Inspecting an Image SBOM

BuildKit uses its default Syft scanner and attaches one SPDX document per platform. Read one without pulling the image:

```bash
docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest \
--format '{{ json (index .SBOM "linux/amd64").SPDX }}'
```

Validate the final attestation, requiring a Cargo package for an auditable image:

```bash
tasks/scripts/verify-image-sbom.sh ghcr.io/nvidia/openshell/gateway:latest --require-cargo
```

## Inspecting an Auditable Image Binary

Opt into auditable metadata when staging a local image binary:

```bash
OPENSHELL_AUDITABLE=1 PREBUILT_ARCH=amd64 \
tasks/scripts/stage-prebuilt-binaries.sh gateway
```

Scan the staged binary rather than the source tree:

```bash
mise x -- syft \
"file:deploy/docker/.build/prebuilt-binaries/amd64/openshell-gateway" \
-o cyclonedx-json
```

This output is limited to packages Syft discovers from that binary. Use
`mise run sbom` for the broader source-tree license-compliance inventory.

## Workflow 1: Full SBOM Generation (One Command)

```bash
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/branch-e2e.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ jobs:
with:
component: gateway
image-tag: ${{ github.sha }}
auditable: true

build-supervisor:
needs: [pr_metadata]
Expand All@@ -97,6 +98,7 @@ jobs:
with:
component: supervisor
image-tag: ${{ github.sha }}
auditable: true

build-cli:
needs: [pr_metadata]
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/docker-build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,11 @@ on:
required: false
type: string
default: ""
auditable:
description: "Embed cargo-auditable dependency metadata in image binaries"
required: false
type: boolean
default: false
image-tag:
description: "Image tag base to build/push (defaults to the GitHub SHA)"
required: false
Expand DownExpand Up@@ -177,6 +182,7 @@ jobs:
image-tag: ${{ needs.resolve.outputs.image_tag_base }}
checkout-ref: ${{ inputs['checkout-ref'] }}
features: ${{ needs.resolve.outputs.features }}
auditable: ${{ inputs.auditable }}
artifact-name: ${{ needs.resolve.outputs.artifact_prefix }}-linux-${{ matrix.arch }}
secrets: inherit

Expand DownExpand Up@@ -288,6 +294,10 @@ jobs:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs['checkout-ref'] || github.sha }}

- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

Expand All@@ -303,3 +313,6 @@ jobs:
--prefer-index=false \
-t "${image}:${{ needs.resolve.outputs.image_tag_base }}" \
"${refs[@]}"

- name: Verify merged manifest SBOM attestation
run: tasks/scripts/verify-image-sbom.sh "ghcr.io/nvidia/openshell/${{ inputs.component }}:${{ needs.resolve.outputs.image_tag_base }}" ${{ inputs.auditable && '--require-cargo' || '' }}
2 changes: 2 additions & 0 deletions .github/workflows/release-dev.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,13 +59,15 @@ jobs:
with:
component: gateway
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
auditable: true

build-supervisor:
needs: [compute-versions]
uses: ./.github/workflows/docker-build.yml
with:
component: supervisor
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
auditable: true

e2e:
needs: [build-gateway, build-supervisor]
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release-tag.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,7 @@ jobs:
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
image-tag: ${{ needs.compute-versions.outputs.source_sha }}
checkout-ref: ${{ inputs.tag || github.ref }}
auditable: true

build-supervisor:
needs: [compute-versions]
Expand All@@ -90,6 +91,7 @@ jobs:
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
image-tag: ${{ needs.compute-versions.outputs.source_sha }}
checkout-ref: ${{ inputs.tag || github.ref }}
auditable: true

e2e:
needs: [compute-versions, build-gateway, build-supervisor]
Expand Down
31 changes: 30 additions & 1 deletion .github/workflows/rust-native-build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,11 @@ on:
required: false
type: string
default: ""
auditable:
description: "Embed cargo-auditable dependency metadata in the binary"
required: false
type: boolean
default: false
retention-days:
description: "Artifact retention period"
required: false
Expand DownExpand Up@@ -265,6 +270,7 @@ jobs:
mise x -- rustup target add "${{ steps.target.outputs.target }}"

cargo_cmd=(cargo build)
cargo_env=()
build_target="${{ steps.target.outputs.target }}"
args=()

Expand All@@ -278,6 +284,13 @@ jobs:
# accepts -static for *-linux-gnu and links dynamically anyway.
export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+crt-static"
fi
if [[ "${{ inputs.auditable }}" == "true" ]]; then
cargo_cmd=("${cargo_cmd[0]}" auditable "${cargo_cmd[@]:1}")
# mise.toml injects RUSTC_WRAPPER=sccache. Unset it after mise has
# constructed the environment so it cannot wrap cargo-auditable's
# RUSTC_WORKSPACE_WRAPPER and misidentify that wrapper as rustc.
cargo_env=(env -u RUSTC_WRAPPER)
fi
args+=(
--release
--target "$build_target"
Expand All@@ -290,7 +303,7 @@ jobs:
if [[ -n "${{ steps.version.outputs.cargo_version }}" ]]; then
export GIT_DIR=/nonexistent
fi
mise x -- "${cargo_cmd[@]}" "${args[@]}"
mise x -- "${cargo_env[@]}" "${cargo_cmd[@]}" "${args[@]}"

- name: Verify packaged binary
run: |
Expand All@@ -307,6 +320,22 @@ jobs:
exit 1
fi

- name: Verify auditable dependency metadata
if: inputs.auditable
run: |
set -euo pipefail
BIN="target/${{ steps.target.outputs.target }}/release/${{ steps.target.outputs.binary }}"
cargo_packages="$(
SYFT_CHECK_FOR_APP_UPDATE=false \
mise x -- syft "file:$BIN" -o cyclonedx-json |
jq '[.components[]? | select((.purl // "") | startswith("pkg:cargo/"))] | length'
)"
if [[ "$cargo_packages" -eq 0 ]]; then
echo "Syft did not decode any Cargo packages from $BIN" >&2
exit 1
fi
echo "auditable dependency metadata: $cargo_packages Cargo packages"

- name: Verify glibc symbol floor
if: inputs.component == 'gateway'
run: |
Expand Down
33 changes: 33 additions & 0 deletions architecture/build.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,39 @@ step via the `rust-native-build.yml` workflow (per-architecture, per-component)
and uploads the result as an artifact that the image build job downloads back
into the staging directory before running Buildx.

Gateway and supervisor binaries staged into branch E2E, Release Dev, and Release
Tag images are compiled through `cargo auditable` (pinned in `mise.toml`), which
embeds a `.dep-v0` section describing the Rust dependencies actually compiled
into the binary. That section holds data rather than symbols, so it survives the
workspace's `strip = true` release profile, and Syft can catalog the crates
present in image binaries instead of inferring them from the source tree. This
is a different artifact from the source SBOM produced by `syft dir:.` in
`tasks/sbom.toml`, which describes the checkout, and from the image SBOM
attestation below, which describes a published image.

`docker-build.yml` and `rust-native-build.yml` take an `auditable` input that
defaults to false. The branch E2E gateway and supervisor image builds set it to
true so E2E exercises release-parity image binaries; other PR image builds and
standalone release artifacts stay non-auditable. The CI image gains the pinned
`cargo-auditable` tool through `mise install --locked` but ships no auditable
OpenShell binary of its own. Local staging opts in with
`OPENSHELL_AUDITABLE=1`. sccache's `RUSTC_WRAPPER` is unset only around auditable
builds, because it would otherwise wrap `cargo-auditable`'s workspace wrapper and
be misidentified as `rustc`. Auditable builds are verified by scanning the built
binary with Syft and requiring at least one decoded Cargo package; the check runs
only for those builds.

Pushed Docker images carry minimal SLSA provenance and a per-platform SPDX SBOM
generated by BuildKit's default Syft scanner. The registry exporter uses OCI
media types and `oci-artifact=true`, so each attestation identifies its subject.
GHCR exposes these through the image index because it has no referrers API.

Attestations require a registry-backed image index. Local builds therefore keep
`--provenance=false`, and Podman builds carry neither attestation.
`tasks/scripts/verify-image-sbom.sh` verifies the merged multi-arch tag and runs
with `--require-cargo` for auditable builds, so those attestations must also
contain Cargo packages.

Runtime layout:

- **Gateway**: `gcr.io/distroless/cc-debian13:nonroot` base, GNU-linked binary at
Expand Down
36 changes: 36 additions & 0 deletions docs/security/verifying-images.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: "Verify the Contents of Published OpenShell Images"
sidebar-title: "Verify Image Contents"
slug: "security/verify-image-contents"
description: "Read the SBOM attestation attached to published gateway and supervisor images to audit what each image contains."
keywords: "Generative AI, Cybersecurity, Supply Chain, SBOM, Container Images"
position: 2
---

Published gateway and supervisor images carry one SPDX SBOM per platform as OCI attestations.

## Inspect an Image

Read a platform's document without pulling the image:

```shell
docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest --format '{{ json (index .SBOM "linux/amd64").SPDX }}'
```

List the packages instead of the full document:

```shell
docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest --format '{{ range (index .SBOM "linux/amd64").SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}'
```

The same commands work for `ghcr.io/nvidia/openshell/supervisor`.

## Coverage

Every SBOM lists the base-image packages. Release Dev and Release Tag images also list the Rust crates compiled into their OpenShell binary.

<Note>
OpenShell also publishes minimal SLSA provenance. It records how BuildKit produced the image, including its source revision, build platform, and base-image materials, without the extra build parameters included by full provenance.
</Note>
24 changes: 24 additions & 0 deletions mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mise.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ k3d = { version = "5.8.3", os = ["macos"] }
"github:EmbarkStudios/cargo-about" = { version = "0.8.4", version_prefix = "" }
"github:EmbarkStudios/cargo-deny" = { version = "0.20.2", version_prefix = "" }
zig = "0.14.1"
"github:rust-secure-code/cargo-auditable" = "0.7.5"
"github:rust-cross/cargo-zigbuild" = "0.22.3"
"npm:markdownlint-cli2" = "0.22.0"

Expand Down
15 changes: 10 additions & 5 deletions tasks/scripts/docker-build-image.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,14 +168,19 @@ if [[ "${IS_FINAL_IMAGE}" == "1" ]]; then
TAG_ARGS=(-t "${IMAGE_NAME}:${IMAGE_TAG}")
fi

ATTESTATION_ARGS=(--provenance=false)
OUTPUT_ARGS=()
if [[ -n "${DOCKER_OUTPUT:-}" ]]; then
OUTPUT_ARGS=(--output "${DOCKER_OUTPUT}")
elif [[ "${IS_FINAL_IMAGE}" == "1" ]]; then
if [[ "${DOCKER_PUSH:-}" == "1" ]]; then
OUTPUT_ARGS=(--push)
elif [[ "${DOCKER_PLATFORM:-}" == *","* ]]; then
OUTPUT_ARGS=(--push)
if [[ "${DOCKER_PUSH:-}" == "1" || "${DOCKER_PLATFORM:-}" == *","* ]]; then
if ce_is_docker; then
# Attestations require a registry-backed image index.
ATTESTATION_ARGS=(--provenance=mode=min --attest type=sbom)
OUTPUT_ARGS=(--output "type=image,push=true,oci-mediatypes=true,oci-artifact=true")
else
OUTPUT_ARGS=(--push)
fi
else
OUTPUT_ARGS=(--load)
fi
Expand All@@ -191,7 +196,7 @@ ce_build \
-f "${DOCKERFILE}" \
--target "${DOCKER_TARGET}" \
${TAG_ARGS[@]+"${TAG_ARGS[@]}"} \
--provenance=false \
${ATTESTATION_ARGS[@]+"${ATTESTATION_ARGS[@]}"} \
"$@" \
${OUTPUT_ARGS[@]+"${OUTPUT_ARGS[@]}"} \
.
Loading
Loading