ci: the supply chain accounts for itself — per-release SBOM, full-tree audits, a real license gate

CRA Annex I Part II groundwork (see punktfunk-planning design/cra-readiness.md, Phase 1):

* sbom.yml + scripts/ci/gen-sbom.sh: every vX.Y.Z release gets a CycloneDX
  SBOM attached — syft over both Cargo.locks, all Bun/pnpm trees and the
  Swift Package.resolved (2,667 components), merged with
  compliance/sbom/manual-components.cdx.json for what no lockfile records
  (pyrowave/Granite/volk/Vulkan-Headers pins, libvpl, FFmpeg, SDL3,
  VB-CABLE, punktfunk-gamescope).
* audit.yml: bun audit now covers sdk + plugin-kit (not just web), decky's
  pnpm tree is scanned, and docs-site runs non-blocking until its known
  CMS-chain advisories are cleared. All shipping trees verified green today.
* license-gate: about.toml's allowlist claim is finally enforced —
  cargo-about 0.9.1 with --fail over BOTH workspaces. The old [crate.clarify]
  license-only syntax fails to deserialize under 0.9; migrated ring to a
  per-crate accepted extension and dropped the stale aws-lc-sys entry
  (workspace is ring-only). Both gates validated green locally.
* drivers/Cargo.lock: sync the pf-dualsense→pf-gamepad rename — the crate
  rename updated the manifest but the Windows-only lockfile was never
  regenerated; cargo-about's metadata pass caught it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 22:21:59 +02:00
co-authored by Claude Fable 5
parent cf419d8b6f
commit c14308bf71
6 changed files with 280 additions and 22 deletions
+97 -11
View File
@@ -1,9 +1,22 @@
# Supply-chain advisory scan for BOTH dependency trees the project ships to users:
# Supply-chain advisory scan for EVERY dependency tree the project ships or publishes, plus the
# license-allowlist gate (CRA Annex I Part II: know your components; catch a bad dep the moment
# it lands).
# * cargo-audit → the (network-facing, crypto-heavy) Rust tree, against the RustSec advisory DB.
# * bun audit → the web management console (Nitro/Bun BFF) — the component that holds the login
# gate, session sealing, and the mgmt bearer token, so its deps matter too.
# Triggers: weekly (catch newly-disclosed CVEs in pinned deps), on every lockfile change (catch a bad
# dep the moment it lands), and on demand.
# * bun audit → each Bun-managed tree that ships or publishes: web (the mgmt console BFF —
# login gate, session sealing, mgmt bearer token), sdk (@punktfunk/host),
# plugin-kit (@punktfunk/plugin-kit).
# * pnpm audit → clients/decky (the Steam Deck plugin).
# * docs-site → scanned NON-blocking (continue-on-error): known transitive advisories ride in
# via the CMS/UI chain (@unom/ui → payload → dompurify/monaco) and the nitropack
# build chain (node-tar, brace-expansion); clearing them needs coordinated bumps
# verified against the LIVE site (the docs don't build standalone) — tracked in
# punktfunk-planning design/cra-readiness.md. Flip to blocking once clean.
# * cargo-about → license-allowlist gate over BOTH Rust workspaces (about.toml `accepted`);
# fails if any crate carries a license outside the allowlist — the regression
# guard about.toml always promised. (The Android Gradle tree has no lockfile, so
# nothing scans it — see the CRA roadmap.)
# Triggers: weekly (catch newly-disclosed CVEs in pinned deps), on every lockfile/allowlist
# change, and on demand.
# To silence a known-unfixable Rust advisory, add it to `.cargo/audit.toml` ([advisories] ignore=[…]).
name: audit
@@ -12,7 +25,16 @@ on:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
push:
branches: [main]
paths: ['Cargo.lock', 'web/bun.lock', '.gitea/workflows/audit.yml']
paths:
- 'Cargo.lock'
- 'packaging/windows/drivers/Cargo.lock'
- 'web/bun.lock'
- 'docs-site/bun.lock'
- 'sdk/bun.lock'
- 'plugin-kit/bun.lock'
- 'clients/decky/pnpm-lock.yaml'
- 'about.toml'
- '.gitea/workflows/audit.yml'
workflow_dispatch:
jobs:
@@ -36,13 +58,17 @@ jobs:
cargo audit
bun-audit:
strategy:
fail-fast: false
matrix:
tree: [web, sdk, plugin-kit]
runs-on: ubuntu-24.04
container:
image: oven/bun:1
timeout-minutes: 15
defaults:
run:
working-directory: web
working-directory: ${{ matrix.tree }}
steps:
# oven/bun's slim base lacks a CA bundle + git — actions/checkout's HTTPS fetch needs them
# (same preamble as web-screenshots.yml / ci.yml's web job).
@@ -50,9 +76,69 @@ jobs:
working-directory: /
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates git
- uses: actions/checkout@v4
# `bun audit` queries the registry advisory DB for the versions pinned in web/bun.lock. No
# install/build needed — it reads the manifest + lockfile. Fails the job on any advisory, the
# same fail-on-vulnerability stance as cargo-audit above; triage a finding by bumping the dep
# (or, if genuinely unfixable + inapplicable, pinning a resolution and noting why here).
# `bun audit` queries the registry advisory DB for the versions pinned in the tree's
# bun.lock. No install/build needed — it reads the manifest + lockfile. Fails the job on any
# advisory, the same fail-on-vulnerability stance as cargo-audit above; triage a finding by
# bumping the dep (or, if genuinely unfixable + inapplicable, pinning a resolution and
# noting why here).
- name: bun audit
run: bun audit
# Kept OUT of the bun-audit matrix so this tree's known-advisory state can't normalize failure
# in a shipping tree. See the header for why this one is non-blocking.
docs-site-audit:
runs-on: ubuntu-24.04
container:
image: oven/bun:1
timeout-minutes: 15
continue-on-error: true
defaults:
run:
working-directory: docs-site
steps:
- name: Install git + CA certs
working-directory: /
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates git
- uses: actions/checkout@v4
- name: bun audit (non-blocking)
run: bun audit
pnpm-audit:
runs-on: ubuntu-24.04
container:
image: node:22-bookworm
timeout-minutes: 15
defaults:
run:
working-directory: clients/decky
steps:
- uses: actions/checkout@v4
# decky is pnpm-managed (pnpm-lock.yaml lockfileVersion 9.0 → pnpm 10 reads it). Like
# bun audit, `pnpm audit` needs no install/build — lockfile + registry advisory DB only.
- name: pnpm audit
run: |
npm install -g pnpm@10
pnpm audit
# The regression guard about.toml documents: fail if any crate in either Rust workspace carries
# a license outside the `accepted` allowlist (e.g. a copyleft dep silently entering the linked
# set). cargo-about is version-pinned: the config uses the per-crate `accepted` syntax
# validated against exactly this version.
license-gate:
runs-on: ubuntu-24.04
container:
image: git.unom.io/unom/punktfunk-rust-ci:latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: /usr/local/cargo
key: cargo-about-0.9.1
restore-keys: cargo-about-
- name: cargo about license gate (host + driver workspaces)
run: |
git config --global --add safe.directory "$PWD"
command -v cargo-about >/dev/null 2>&1 || cargo install --locked cargo-about --version 0.9.1 --features cli
cargo about generate about.hbs --fail -o /dev/null
cargo about generate -m packaging/windows/drivers/Cargo.toml -c about.toml about.hbs --fail -o /dev/null
+60
View File
@@ -0,0 +1,60 @@
# Per-release SBOM (CRA Annex I Part II §1: identify and document the components in the product,
# in a commonly used machine-readable format — we emit CycloneDX JSON).
#
# Tag push → the SBOM is attached to the Gitea release, next to the artifacts it describes.
# Release assets are never pruned (security updates must stay available ≥10 years, CRA Art. 13),
# so the SBOM's retention rides on the release's.
# workflow_dispatch on a non-tag ref → generated and uploaded as a workflow artifact only
# (pipeline validation / an on-demand snapshot); no release is touched.
#
# What goes in: scripts/ci/gen-sbom.sh = syft over the checkout (every lockfile-pinned dep in
# both Rust workspaces + the JS trees + Swift Package.resolved) merged with
# compliance/sbom/manual-components.cdx.json (vendored C/C++, bundled DLLs, VB-CABLE, gamescope).
name: sbom
on:
push:
tags: ['v*']
workflow_dispatch:
jobs:
sbom:
runs-on: ubuntu-24.04
container:
image: git.unom.io/unom/punktfunk-rust-ci:latest
timeout-minutes: 20
steps:
# fetch-depth 0: the dispatch path derives the canary base from the tag history
# (scripts/ci/pf-version.sh), which a shallow clone cannot see.
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Pinned syft (keep in sync with the version validated against this repo; bump deliberately).
- name: Install syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
| sh -s -- -b /usr/local/bin v1.49.0
- name: Generate SBOM
run: |
git config --global --add safe.directory "$PWD"
case "$GITHUB_REF" in
refs/tags/v*) VERSION="${GITHUB_REF_NAME#v}" ;;
*) eval "$(bash scripts/ci/pf-version.sh)"; VERSION="${PF_BASE}-snapshot" ;;
esac
sh scripts/ci/gen-sbom.sh "$VERSION" "punktfunk-${VERSION}.cdx.json"
echo "SBOM_FILE=punktfunk-${VERSION}.cdx.json" >> "$GITHUB_ENV"
- name: Attach to release
if: startsWith(github.ref, 'refs/tags/')
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
. scripts/ci/gitea-release.sh
RID=$(ensure_release "$GITHUB_REF_NAME" "$GITHUB_REF_NAME" auto)
upsert_asset "$RID" "$SBOM_FILE"
# v3, not v4: Gitea's artifact backend rejects upload-artifact@v4 (see release.yml).
- name: Upload artifact (non-tag runs)
if: "!startsWith(github.ref, 'refs/tags/')"
uses: actions/upload-artifact@v3
with:
name: sbom
path: punktfunk-*.cdx.json
+12 -10
View File
@@ -37,13 +37,15 @@ accepted = [
ignore-build-dependencies = true
ignore-dev-dependencies = true
# r-efi offers an LGPL-2.1-or-later arm but is tri-licensed; take a permissive arm. (It is also
# UEFI-target-gated out of every shipped build.)
[r-efi.clarify]
license = "MIT OR Apache-2.0"
[ring.clarify]
license = "MIT AND ISC AND OpenSSL"
[aws-lc-sys.clarify]
license = "ISC AND Apache-2.0 AND MIT AND BSD-3-Clause AND OpenSSL"
# Per-crate license-acceptance additions (cargo-about ≥0.6 syntax; the old `[crate.clarify]`
# license-only form fails to deserialize under cargo-about 0.9, which now wants checksummed file
# clarifications — per-crate `accepted` extensions express the same intent without checksums).
#
# r-efi is tri-licensed with an LGPL-2.1-or-later arm; cargo-about resolves OR-expressions to an
# accepted arm on its own (MIT/Apache-2.0 are globally accepted), so it needs no entry. (It is
# also UEFI-target-gated out of every shipped build.)
#
# ring's license is an AND of permissive terms including the OpenSSL license; accept the
# OpenSSL/ISC parts for this crate only, not globally.
[ring]
accepted = ["OpenSSL", "ISC"]
@@ -0,0 +1,79 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": [
{
"type": "library",
"name": "pyrowave",
"version": "509e4f887b585a3f97471fcc804e9de649f2c16f",
"description": "GPU wavelet codec, vendored at crates/pyrowave-sys/vendor/pyrowave (pruned tree) with local patches from crates/pyrowave-sys/patches/; commits recorded in vendor/pyrowave/PUNKTFUNK-VENDOR.txt",
"licenses": [{ "license": { "id": "MIT" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/Themaister/pyrowave" }]
},
{
"type": "library",
"name": "Granite",
"version": "44362775d36e0c4139352f83efd96bab4e239f66",
"description": "Vulkan backend subset vendored inside the pyrowave tree",
"licenses": [{ "license": { "id": "MIT" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/Themaister/Granite" }]
},
{
"type": "library",
"name": "volk",
"version": "47cddf7ed97b94118a08aacb548a411188e016cc",
"description": "Vulkan meta-loader vendored inside the pyrowave/Granite tree",
"licenses": [{ "license": { "id": "MIT" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/zeux/volk" }]
},
{
"type": "library",
"name": "Vulkan-Headers",
"version": "015e25c3c91b70eb1a754d36fb14c4ba6ad9b0b9",
"description": "Vulkan API headers vendored inside the pyrowave tree",
"licenses": [{ "license": { "id": "Apache-2.0" } }, { "license": { "id": "MIT" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/KhronosGroup/Vulkan-Headers" }]
},
{
"type": "library",
"name": "libvpl",
"version": "2.17.0",
"description": "Intel oneVPL dispatcher, vendored at crates/libvpl-sys/vendor/libvpl",
"licenses": [{ "license": { "id": "MIT" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/intel/libvpl" }]
},
{
"type": "library",
"name": "FFmpeg",
"version": "7.x/8.x (system-provided on Linux; replaceable DLLs bundled with the Windows packages)",
"description": "Dynamically linked libav* decode/encode; LGPL notice at packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt",
"licenses": [{ "license": { "id": "LGPL-2.1-or-later" } }],
"externalReferences": [{ "type": "website", "url": "https://ffmpeg.org" }]
},
{
"type": "library",
"name": "SDL3",
"version": "3.x (system-provided or bundled per platform package)",
"description": "Windowing/input layer for the desktop clients, dynamically linked",
"licenses": [{ "license": { "id": "Zlib" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/libsdl-org/SDL" }]
},
{
"type": "application",
"name": "VB-CABLE",
"version": "redistributed installer, see packaging/windows/install-vbcable.ps1",
"description": "Third-party kernel-mode virtual audio driver redistributed with the Windows host; notice at packaging/windows/licenses/VB-CABLE-NOTICE.txt. Planned to be replaced by an attestation-signed first-party driver.",
"licenses": [{ "license": { "name": "Proprietary freeware (VB-Audio Software, redistribution permitted per notice)" } }],
"externalReferences": [{ "type": "website", "url": "https://vb-audio.com/Cable/" }]
},
{
"type": "application",
"name": "punktfunk-gamescope",
"version": "upstream gamescope pinned by packaging/nix/gamescope.nix (nixpkgs) or built by packaging/gamescope/build-punktfunk-gamescope.sh, plus 3 local patches from packaging/gamescope/patches/",
"description": "Patched gamescope compositor distributed via sysext/Arch/nix channels alongside the host",
"licenses": [{ "license": { "id": "BSD-2-Clause" } }],
"externalReferences": [{ "type": "vcs", "url": "https://github.com/ValveSoftware/gamescope" }]
}
]
}
+1 -1
View File
@@ -402,7 +402,7 @@ dependencies = [
]
[[package]]
name = "pf-dualsense"
name = "pf-gamepad"
version = "0.0.1"
dependencies = [
"pf-driver-proto",
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
# Generate the per-release CycloneDX SBOM (CRA Annex I Part II §1 — machine-readable component
# inventory, attached to every stable release by .gitea/workflows/sbom.yml).
#
# syft walks the checkout and catalogs every lockfile-pinned dependency (both Rust workspaces via
# their Cargo.locks, the Bun/pnpm/npm trees, the Swift Package.resolved);
# compliance/sbom/manual-components.cdx.json contributes the components no lockfile records —
# vendored C/C++ trees (pyrowave/Granite/volk/Vulkan-Headers, libvpl), dynamically-linked/bundled
# libraries (FFmpeg, SDL3), the redistributed VB-CABLE driver, and the patched gamescope. Keep
# that file current when vendoring changes (scripts/vendor-pyrowave.sh etc.).
#
# Usage: scripts/ci/gen-sbom.sh VERSION [OUTPUT]
# Requires: syft (pinned install in the workflow), python3 (a proven runner dependency).
set -eu
VERSION="${1:?usage: gen-sbom.sh VERSION [OUTPUT]}"
OUT="${2:-punktfunk-${VERSION}.cdx.json}"
TMP="${OUT}.syft.tmp"
syft scan "dir:." --source-name punktfunk --source-version "$VERSION" \
-o "cyclonedx-json=$TMP" -q
OUT="$OUT" TMP="$TMP" python3 - <<'PY'
import json, os
gen = json.load(open(os.environ["TMP"]))
manual = json.load(open("compliance/sbom/manual-components.cdx.json"))
gen.setdefault("components", []).extend(manual["components"])
with open(os.environ["OUT"], "w") as f:
json.dump(gen, f, indent=2)
print("SBOM: %d components -> %s" % (len(gen["components"]), os.environ["OUT"]))
PY
rm -f "$TMP"