ci(security): add a supply-chain gate, checksums and an SBOM

The project shipped signed Android builds and unsigned desktop binaries
with no vulnerability scanning of any kind. Nothing checked the ~500
crate Rust graph or the JS packages against an advisory feed, and nothing
checked that what we redistribute inside an MIT bundle permits it.

The first cargo-deny run found eight vulnerabilities and one
unsoundness -- bytes, four in rustls-webpki, time, two in quick-xml and
rand -- every one of them closed by a `cargo update` nobody had a reason
to run. That update is in this commit; 740 Rust tests and clippy
-D warnings pass on the new lockfile.

Two structural fixes matter as much as the gate itself:

  - deny.toml scopes the graph to the targets we actually ship. Without
    it the Apple targets pull in plist -> quick-xml and report two DoS
    advisories against a crate that is in no binary we release. Ignoring
    those by ID would silence them everywhere, including where they
    would matter; scoping makes them correctly absent.

  - libmpv is pinned by rev instead of branch = "master". A branch means
    the revision is whatever Cargo.lock happens to hold and any
    `cargo update` silently substitutes new upstream code -- in the one
    dependency that is not from crates.io and that links a C library
    into the player. The rev is the commit already locked, so this pins
    current behaviour rather than changing it.

Licence findings are recorded rather than waved through. libmpv and
libmpv-sys are LGPL-2.1, satisfied here by dynamic linking against the
system library; deny.toml carries the two obligations that follow (keep
the linkage dynamic, ship libmpv's licence text with any bundle carrying
the .so). MPL-2.0 crates are file-level copyleft and fine unmodified.

Releases now publish SHA256SUMS (verified in-job with `sha256sum -c`
before upload) and a CycloneDX SBOM for both halves, so "does this
release contain <vulnerable crate>?" has an answer that is not "rebuild
the tag and re-resolve it".

Workflows pin jellytau-builder:2026.08 instead of :latest. While every
job said :latest, rebuilding the image changed what every build compiled
against, including rebuilds of old release tags.

Also folded in, because both were the same class of problem:

  - publish-docs.yml downloaded mdBook from GitHub releases into
    /usr/local/bin at job time -- a toolchain install in CI, which
    CLAUDE.md explicitly forbids, and a hard dependency on GitHub's CDN
    at publish time. It is in the builder image now.

  - extract-traces.ts only ever read .ts/.svelte/.rs, so every
    requirement implemented by *configuration* was invisible to the
    matrix that measures it. DR-205, DR-206, DR-207 and DR-215 all carry
    TRACES comments nothing read, and each counted as uncovered while
    being covered. Coverage was really 90%, not 88%; MIN_THRESHOLD moves
    to 89 accordingly. CI workflows stay excluded and there is a test
    saying why: traceability-check.yml quotes "a TRACES: comment" beside
    deliberately-undefined example IDs, which the extractor would read
    as real traces and then fail its own dangling-ID check.

Supply-chain requirement is DR-216.

🔴 The builder image must be rebuilt and pushed
(scripts/build-builder-image.sh 2026.08) before this reaches master --
the workflows now name a tag and tools that do not exist in the registry
yet.
This commit is contained in:
2026-08-21 18:25:38 +02:00
parent 32043a2152
commit f6653e6a8b
13 changed files with 504 additions and 60 deletions
+59 -2
View File
@@ -28,7 +28,7 @@ jobs:
if: "!startsWith(github.event.head_commit.message, 'chore(release)')"
runs-on: linux/amd64
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
@@ -156,7 +156,7 @@ jobs:
runs-on: linux/amd64
needs: test
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
env:
ANDROID_HOME: /opt/android-sdk
ANDROID_SDK_ROOT: /opt/android-sdk
@@ -208,3 +208,60 @@ jobs:
export AR_aarch64_linux_android="$TC/llvm-ar"
cd src-tauri
cargo check --target aarch64-linux-android --lib
# Supply-chain gate. Until this job existed the project had no vulnerability
# scanning of any kind: nothing checked the ~500-crate Rust graph or the JS
# dependencies against a CVE feed, and nothing checked that everything we
# redistribute is licence-compatible with shipping JellyTau under MIT.
#
# The first run of this found eight vulnerabilities and one unsoundness
# (bytes, four in rustls-webpki, time, two in quick-xml, rand) — all fixed by
# `cargo update`, none of which anybody had reason to run.
#
# Runs in parallel with android-check rather than after `test`: a dependency
# advisory has nothing to do with whether the tests pass, and finding out
# sooner is the point.
security:
name: Supply Chain
runs-on: linux/amd64
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-
# cargo-deny is baked into the builder image. It fetches the RustSec
# advisory database at run time — that is *data*, like the crates
# `bun install` fetches, not a toolchain install, so the 🔴 rule in
# CLAUDE.md is not in play here.
#
# Config and every documented exception live in src-tauri/deny.toml.
# Vulnerabilities and unsoundness are hard failures with no override;
# unmaintained transitive crates that have no safe upgrade (Tauri's GTK3
# stack, the unic-* tables) are ignored there by ID, each with a reason.
- name: cargo-deny (advisories, licences, bans, sources)
run: |
cd src-tauri
cargo deny check
# Advisory for now, deliberately. The Rust graph was clean after one
# update pass, so gating it costs nothing; the JS graph has not been
# audited before and a first run that fails the build teaches everyone to
# ignore this job. Promote to a hard gate once the output is empty and
# stays empty — same approach that got clippy from advisory to -D warnings.
- name: bun audit (advisory)
run: |
bun install
bun audit || echo "::warning::bun audit reported findings — advisory for now, see CLAUDE.md"
+45 -6
View File
@@ -21,7 +21,7 @@ jobs:
name: Run Tests
runs-on: linux/amd64
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -94,7 +94,7 @@ jobs:
runs-on: linux/amd64
needs: test
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -190,7 +190,7 @@ jobs:
# baked into the builder image. No toolchain installs here — the image has
# cargo-xwin, clang/clang-cl, lld, llvm, nsis and the msvc target.
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -260,7 +260,7 @@ jobs:
runs-on: linux/amd64
needs: test
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
env:
ANDROID_HOME: /opt/android-sdk
ANDROID_SDK_ROOT: /opt/android-sdk
@@ -359,7 +359,7 @@ jobs:
needs: [build-linux, build-windows, build-android]
if: startsWith(github.ref, 'refs/tags/v')
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -388,6 +388,42 @@ jobs:
name: jellytau-android
path: artifacts/android/
# Software Bill of Materials, one per half of the app. Without it there is
# no answer to "does this release contain <vulnerable crate>?" other than
# rebuilding the tag and re-resolving it. cargo-cyclonedx is in the builder
# image; the JS side is read straight from the lockfile bun install used.
- name: Generate SBOM
run: |
set -e
mkdir -p artifacts/sbom
cd src-tauri
cargo cyclonedx --format json
find . -maxdepth 2 -name "*.cdx.json" -exec cp -v {} ../artifacts/sbom/ \;
cd ..
bun install --frozen-lockfile
bun pm ls --all > artifacts/sbom/frontend-dependencies.txt
ls -lah artifacts/sbom/
# Checksums over everything being published. A release of unsigned Linux
# and Windows binaries with no checksum gives a user no way at all to tell
# a corrupted or substituted download from a good one — and the AppImage
# and NSIS installer are both fetched over plain HTTP redirects.
#
# Written with paths relative to the asset directory so `sha256sum -c
# SHA256SUMS` works in the directory a user downloaded into.
- name: Generate SHA256SUMS
run: |
set -e
mkdir -p artifacts/release
find artifacts/linux artifacts/windows artifacts/android -type f -exec cp -v {} artifacts/release/ \;
cd artifacts/release
sha256sum * > SHA256SUMS
echo "🔐 Published checksums:"
cat SHA256SUMS
# Verify what we just wrote, so a broken checksum file fails the
# release rather than shipping and failing for users.
sha256sum -c SHA256SUMS
- name: Prepare release notes
id: release_notes
run: |
@@ -485,7 +521,10 @@ jobs:
fi
echo "Release id=$RELEASE_ID"
for f in artifacts/android/* artifacts/linux/* artifacts/windows/*; do
# artifacts/release/ holds a copy of every platform artifact plus the
# SHA256SUMS generated over exactly that set, so the checksums describe
# precisely what is uploaded. artifacts/sbom/ rides along.
for f in artifacts/release/* artifacts/sbom/*; do
[ -f "$f" ] || continue
echo "⬆️ Uploading $(basename "$f")"
curl -fsS -X POST \
+8 -9
View File
@@ -21,7 +21,7 @@ jobs:
name: Build & publish docs to gitea-pages
runs-on: linux/amd64
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout code
@@ -34,14 +34,13 @@ jobs:
- name: Install dependencies
run: bun install
- name: Install mdBook
run: |
set -e
MDBOOK_VERSION=v0.4.40
URL="https://github.com/rust-lang/mdBook/releases/download/${MDBOOK_VERSION}/mdbook-${MDBOOK_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
echo "⬇️ Downloading mdBook ${MDBOOK_VERSION}"
curl -fsSL "$URL" | tar -xz -C /usr/local/bin
mdbook --version
# mdBook is baked into jellytau-builder (Dockerfile.builder, MDBOOK_VERSION).
# It used to be curl'd from GitHub releases straight into /usr/local/bin
# right here, which was a toolchain install at job time — the exact thing
# CLAUDE.md's 🔴 rule forbids — and made every docs publish depend on
# GitHub's CDN answering. To move the version, bump it in the image.
- name: Confirm mdBook is present
run: mdbook --version
- name: Regenerate traceability matrix (keep published copy current)
run: bun run traces:markdown
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: linux/amd64
name: Check Requirement Traces
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08
steps:
- name: Checkout repository
@@ -94,7 +94,7 @@ jobs:
#
# Keep in sync with MIN_COVERAGE_PERCENT in scripts/extract-traces.ts;
# scripts/extract-traces.test.ts fails if the two drift apart.
MIN_THRESHOLD=88
MIN_THRESHOLD=89
if [ "$COVERAGE" -lt "$MIN_THRESHOLD" ]; then
echo "❌ ERROR: Coverage ($COVERAGE%) is below minimum threshold ($MIN_THRESHOLD%)"
exit 1