Files
dtourolle f6653e6a8b 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.
2026-08-21 18:25:38 +02:00

179 lines
8.6 KiB
TOML

# cargo-deny configuration for the JellyTau backend.
#
# TRACES: | DR-216
#
# Run locally with: cd src-tauri && cargo deny check
# CI runs the same command in the `security` job of
# .gitea/workflows/build-and-test.yml. cargo-deny is baked into the builder
# image (Dockerfile.builder) -- the advisory database it fetches at run time is
# *data*, not a toolchain, so it does not conflict with the "CI installs no
# system tools" rule.
#
# Four checks run: advisories (known vulnerabilities), licenses (what we are
# allowed to ship), bans (duplicate/undesired crates) and sources (where code
# may come from).
# ---------------------------------------------------------------------------
# Graph scope
#
# Only the targets JellyTau actually ships. This is not a performance tweak --
# it changes which advisories are *real*. Without it the graph includes Apple
# targets, which drag in `plist` -> `quick-xml`, and two quick-xml DoS
# advisories (RUSTSEC-2026-0194/0195) get reported against a crate that is not
# compiled into anything we release. Ignoring them by ID would have been the
# wrong fix: it silences the finding everywhere, including on a target where it
# would matter. Scoping the graph makes the finding correctly absent instead.
#
# Add a target here the day we ship it, and expect new findings with it.
[graph]
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"aarch64-linux-android",
"armv7-linux-androideabi",
"x86_64-linux-android",
]
all-features = true
[advisories]
# Vulnerabilities and unsoundness are hard errors -- there is deliberately no
# switch here turning them into warnings. Everything below is an explicit,
# justified exception with a named ID; a new advisory fails the build until
# somebody decides what to do about it.
#
# Yanked crates in the lockfile are an error too: a yank usually means the
# author withdrew that exact version for a reason.
yanked = "deny"
ignore = [
# ---------------------------------------------------------------------
# GTK3 bindings: unmaintained, and not ours to replace.
#
# Tauri v2's Linux backend is WebKitGTK, which is GTK3. The gtk-rs project
# has stopped maintaining its GTK3 bindings in favour of GTK4, but Tauri
# cannot move until WebKitGTK does. These arrive through
# tauri -> tauri-runtime-wry -> wry -> gtk, with no version of any of them
# that avoids it ("No safe upgrade is available", per cargo-deny).
#
# Unmaintained != vulnerable: no advisory here describes an exploitable
# defect. Revisit when Tauri ships a GTK4/WebKitGTK-6 backend.
{ id = "RUSTSEC-2024-0411", reason = "gdkwayland-sys: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0412", reason = "gdk: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0413", reason = "atk: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0414", reason = "gdkx11-sys: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0415", reason = "gtk: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0416", reason = "atk-sys: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0417", reason = "gdkx11: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0418", reason = "gdk-sys: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0419", reason = "gtk3-macros: GTK3 binding, pulled in by Tauri's Linux backend" },
{ id = "RUSTSEC-2024-0420", reason = "gtk-sys: GTK3 binding, pulled in by Tauri's Linux backend" },
# ---------------------------------------------------------------------
# Unmaintained transitive build-time crates. All are proc-macro or
# lookup-table dependencies of Tauri's own toolchain; none has a safe
# upgrade and none is a vulnerability.
{ id = "RUSTSEC-2024-0370", reason = "proc-macro-error: unmaintained proc-macro helper, transitive" },
{ id = "RUSTSEC-2024-0436", reason = "paste: unmaintained macro helper, transitive" },
{ id = "RUSTSEC-2025-0057", reason = "fxhash: unmaintained hasher, transitive" },
# unic-* reach us via urlpattern -> tauri-utils. Unicode table crates,
# superseded upstream but with no drop-in replacement at this depth.
{ id = "RUSTSEC-2025-0075", reason = "unic-char-range: unmaintained, via urlpattern -> tauri-utils" },
{ id = "RUSTSEC-2025-0080", reason = "unic-common: unmaintained, via urlpattern -> tauri-utils" },
{ id = "RUSTSEC-2025-0081", reason = "unic-char-property: unmaintained, via urlpattern -> tauri-utils" },
{ id = "RUSTSEC-2025-0098", reason = "unic-ucd-version: unmaintained, via urlpattern -> tauri-utils" },
{ id = "RUSTSEC-2025-0100", reason = "unic-ucd-ident: unmaintained, via urlpattern -> tauri-utils" },
]
# ---------------------------------------------------------------------------
# Licenses
#
# JellyTau ships as MIT (see ../LICENSE) in deb/rpm/AppImage/NSIS/APK bundles,
# so every crate compiled into those has to be redistributable under terms
# compatible with that. The list is an allow-list on purpose: a new crate with
# an unlisted licence fails the build and gets a decision, rather than being
# shipped because nobody looked.
[licenses]
# A crate offering a choice ("MIT OR Apache-2.0") is satisfied by any allowed
# arm. 0.8 means we accept a licence-file match at >=80% textual confidence.
confidence-threshold = 0.8
allow = [
# Permissive, no redistribution conditions beyond attribution.
"MIT",
"MIT-0",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Zlib",
"0BSD",
"CC0-1.0",
"Unlicense",
"BSL-1.0",
# Unicode data tables (the icu_* family). Permissive, attribution only.
"Unicode-3.0",
# webpki-roots: the Mozilla CA bundle, published as data under CDLA.
"CDLA-Permissive-2.0",
# MPL-2.0 -- weak, *file-level* copyleft (cssparser, selectors, dtoa-short,
# option-ext). The obligation attaches to modified MPL files, not to the
# program that links them, so shipping them unmodified inside an MIT
# application is fine. If we ever patch one of these crates, that patch must
# be published.
"MPL-2.0",
# LGPL-2.1 -- `libmpv` and `libmpv-sys` only, and this one is deliberate.
#
# These are bindings to libmpv, which is itself LGPL-2.1+; the binding
# crates inherit the licence. LGPL permits use from a differently-licensed
# application provided the user can substitute their own build of the
# library, which dynamic linking satisfies -- libmpv-sys links the *system*
# shared object (the builder image installs libmpv-dev; the deb/rpm declare
# a runtime dependency) rather than statically embedding it.
#
# 🔴 Two obligations follow, and they are ours, not cargo-deny's:
# - keep the linkage dynamic (do not switch libmpv-sys to a vendored
# static build without revisiting this),
# - ship libmpv's licence text with any bundle that carries the .so,
# which currently means the AppImage.
"LGPL-2.1",
]
# Crates whose licence field is missing or unparseable get a per-crate
# clarification here rather than a blanket relaxation. Empty today.
exceptions = []
[bans]
# Duplicate versions are noise, not danger: a Tauri-sized graph legitimately
# carries several `windows-sys` and `bitflags` majors because its dependencies
# upgrade at different rates. Warn so the count stays visible; do not fail.
multiple-versions = "warn"
# The only wildcard in the graph is the `libmpv` git dependency: a git dep
# carries no semver requirement, so cargo-deny counts it as `*` no matter how it
# is pinned. It is pinned by `rev` in Cargo.toml and by hash in Cargo.lock, and
# `[sources].allow-git` below is the check that actually constrains it -- so
# "deny" here would fail the build forever over something already controlled
# twice. Warn, so a *second* wildcard still shows up.
wildcards = "warn"
# `cargo build` order for equal-priority features; keeps the check deterministic.
highlight = "all"
deny = []
skip = []
[sources]
# Anything not from crates.io needs to be named here. This is the check that
# would notice a dependency being repointed at somebody's fork.
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# The one git dependency. Cargo.toml pins it by branch, not by revision, which
# is worth knowing: the `master` it resolves to is whatever the lockfile has
# recorded, and `cargo update` will move it. It exists because the crates.io
# release of libmpv predates the MPV versions we support.
allow-git = ["https://github.com/ParadoxSpiral/libmpv-rs.git"]