diff --git a/.gitea/workflows/audit.yml b/.gitea/workflows/audit.yml index 9d0d84ad..8cfc0f79 100644 --- a/.gitea/workflows/audit.yml +++ b/.gitea/workflows/audit.yml @@ -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 diff --git a/.gitea/workflows/sbom.yml b/.gitea/workflows/sbom.yml new file mode 100644 index 00000000..07ace739 --- /dev/null +++ b/.gitea/workflows/sbom.yml @@ -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 diff --git a/about.toml b/about.toml index 845850fa..becd44f7 100644 --- a/about.toml +++ b/about.toml @@ -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"] diff --git a/compliance/sbom/manual-components.cdx.json b/compliance/sbom/manual-components.cdx.json new file mode 100644 index 00000000..56587039 --- /dev/null +++ b/compliance/sbom/manual-components.cdx.json @@ -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" }] + } + ] +} diff --git a/packaging/windows/drivers/Cargo.lock b/packaging/windows/drivers/Cargo.lock index f16d5ec0..1063e830 100644 --- a/packaging/windows/drivers/Cargo.lock +++ b/packaging/windows/drivers/Cargo.lock @@ -402,7 +402,7 @@ dependencies = [ ] [[package]] -name = "pf-dualsense" +name = "pf-gamepad" version = "0.0.1" dependencies = [ "pf-driver-proto", diff --git a/scripts/ci/gen-sbom.sh b/scripts/ci/gen-sbom.sh new file mode 100755 index 00000000..5f514348 --- /dev/null +++ b/scripts/ci/gen-sbom.sh @@ -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"