From a93d84c83b40e39f642a654c9362c2326259a267 Mon Sep 17 00:00:00 2001 From: hellno Date: Sat, 20 Jun 2026 21:41:36 +0200 Subject: [PATCH 1/2] =?UTF-8?q?ci(supply-chain):=20yanks=20warn,=20not=20b?= =?UTF-8?q?lock=20=E2=80=94=20stop=20dep=20drift=20gating=20unrelated=20PR?= =?UTF-8?q?s=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo-deny's advisory check re-reads the live RustSec DB + crates.io yank status on every run, so a yank or fresh CVE could turn an UNRELATED PR red with no change on our side (PR #81 ate a bitcoin_hashes yank). Split the gate by determinism: deterministic bans/licenses/sources still gate PRs; a bare crates.io yank is now warn-level so outside-world drift can't block work that never touched that dep. Real RUSTSEC advisories still block: on PRs, at release, and in the daily scan. - deny.toml: yanked = "deny" -> "warn" (with rationale) - audit.yml: open/refresh a tracking issue on a failed daily scan (gh CLI, no new action) - ci.yml: advisories-job comment reflects the yank policy - docs/AGENTIC-ENGINEERING.md section 4: document the PR-vs-release advisory policy The daily watch + the release-boundary gate are the safety net, not the PR gate. Mainstream Rust-OSS posture; never ship a vulnerable build. --- .github/workflows/audit.yml | 23 +++++++++++++++++++++++ .github/workflows/ci.yml | 12 +++++++----- deny.toml | 11 +++++++++-- docs/AGENTIC-ENGINEERING.md | 24 +++++++++++++++++++++++- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5c8b8ad..af65560 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,6 +16,7 @@ on: permissions: contents: read + issues: write # so a failed scan can open/refresh a tracking issue (#82) jobs: advisories: @@ -25,3 +26,25 @@ jobs: - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check advisories + # Make the daily watch LOUD (#82): a failed scheduled scan means a new RUSTSEC advisory landed + # (yanks are warn-level now, so this fires on real advisories, not yanks). A red scheduled run + # only emails repo admins by default — easy to miss — so open a tracking issue, or comment on + # the existing open one, to de-dup. Uses the preinstalled `gh` CLI: no third-party action added. + - name: Open or refresh advisory tracking issue on failure + if: failure() + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + title="Daily cargo-deny advisory scan is failing" + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + body=$(printf '%s\n\n%s\n\n%s' \ + "The scheduled \`cargo deny check advisories\` run failed (a new RustSec advisory likely landed)." \ + "Failed run: ${run_url}" \ + "Triage in \`deny.toml\`: bump the dependency, or add a justified \`ignore\` entry. Close this issue once the daily scan is green again.") + existing=$(gh issue list --state open --search "${title} in:title" --json number --jq '.[0].number // empty') + if [ -n "${existing}" ]; then + gh issue comment "${existing}" --body "${body}" + else + gh issue create --title "${title}" --body "${body}" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d7f97c..7d5bee7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,11 +108,13 @@ jobs: - run: cargo build --locked -p deckard-app --features tray - run: cargo clippy --locked -p deckard-app --all-targets --features tray -- -D warnings - # Supply-chain SECURITY gate — advisories only. BLOCKING. Config: deny.toml. Every advisory the - # RustSec DB reports is either fixed or carries a written, justified `ignore` in deny.toml; a new, - # untriaged advisory fails this check and blocks the merge until a human triages it. A daily - # re-scan against the committed Cargo.lock runs from .github/workflows/audit.yml. - # Rationale: docs/AGENTIC-ENGINEERING.md §4. + # Supply-chain SECURITY gate — advisories only. BLOCKING on real RUSTSEC advisories. Config: + # deny.toml. Every advisory the RustSec DB reports is either fixed or carries a written, justified + # `ignore` in deny.toml; a new, untriaged advisory fails this check and blocks the merge until a + # human triages it. A bare crates.io *yank* is warn-level (deny.toml `yanked = "warn"`, #82) so + # outside-world drift can't block an unrelated PR — but a real advisory still does, here and at + # release. A daily re-scan against the committed Cargo.lock runs from .github/workflows/audit.yml + # (which also opens a tracking issue on failure). Rationale: docs/AGENTIC-ENGINEERING.md §4. cargo-deny-advisories: needs: quick runs-on: ubuntu-latest diff --git a/deny.toml b/deny.toml index 77214fb..237f164 100644 --- a/deny.toml +++ b/deny.toml @@ -17,11 +17,18 @@ all-features = true [advisories] # RustSec advisory DB. Vulnerabilities and unmaintained crates are denied by default in cargo-deny -# v2; we additionally refuse yanked crates. Add to `ignore` ONLY with a written justification. +# v2 — a real advisory blocks PRs AND releases until it is fixed or given a justified `ignore` below. +# +# `yanked` is WARN, not deny (#82). A crates.io "yank" (a published version withdrawn, often for a +# non-security reason like an accidental publish or a semver slip) is non-deterministic outside-world +# drift: it can flip with no change on our side and was blocking UNRELATED PRs (PR #81 ate a +# `bitcoin_hashes` yank). Warn keeps the yank visible in logs without gating any PR; a yank that is +# also a real security pull still surfaces as a RUSTSEC advisory and still blocks. Add to `ignore` +# ONLY with a written justification. # # Every entry below is a TRANSITIVE advisory with NO in-tree fix; removing them is tracked in #40. # Re-evaluate the whole list on each `just bump-gpui` / Helios / alloy bump — drop any that resolve. -yanked = "deny" +yanked = "warn" ignore = [ # ── rustls-webpki 0.101 — TLS path-validation advisories, reachable only through the Helios # light client's jsonrpsee 0.19 → rustls 0.21 stack. rustls 0.21 caps webpki at 0.101, so diff --git a/docs/AGENTIC-ENGINEERING.md b/docs/AGENTIC-ENGINEERING.md index 75a3e3c..42ed9a5 100644 --- a/docs/AGENTIC-ENGINEERING.md +++ b/docs/AGENTIC-ENGINEERING.md @@ -142,7 +142,7 @@ disallowed licenses, and untrusted sources. New `deny.toml`: ```toml [advisories] -yanked = "deny" +yanked = "warn" # see "Advisory policy" below (#82) — a bare yank shouldn't block unrelated PRs ignore = [] # add { id = "RUSTSEC-…", reason = "…" } only with written justification [licenses] @@ -182,6 +182,28 @@ licenses`), and `multiple-versions` must stay `warn` because the git gpui tree l duplicates crates. We therefore land the CI job **non-blocking** and promote it after one green run (see Rollout). +**Advisory policy — what gates a PR vs what only informs (#82).** Split the gate by *determinism*. +`bans` / `licenses` / `sources` are deterministic — they only change when *we* change dependencies — +so they stay **required, blocking PR gates**. The advisory check is different: it re-reads the live +RustSec database and crates.io yank status on every run, so a dependency yank or a freshly-published +CVE can turn a PR red with **no change on our side**, blocking work that never touched that +dependency (this is what bit PR #81). We handle that by *kind*: + +- **A real RUSTSEC advisory still blocks** — on PRs, at release, and in the daily scan. A known + vulnerability stops the line until it is fixed or given a justified `ignore`. +- **A bare crates.io yank is warn-level** (`yanked = "warn"`). A yank is usually a pulled publish, + not a vulnerability; if it *is* a security pull it also carries a RUSTSEC advisory and still + blocks. So yanks stay visible without gating unrelated PRs. + +The safety net is three things, not the PR gate: continuous **daily detection** +(`.github/workflows/audit.yml`, which opens a tracking issue on failure so a new advisory is loud, +not just an email), a **hard gate at the release boundary** (the reusable `ci.yml` runs the advisory +check blocking when invoked from `release.yml`, so a known-vulnerable tree can never ship), and the +`ignore` list as the **deliberate, reviewed** escape hatch for transitive advisories with no in-tree +fix. This is the mainstream Rust-OSS posture (schedule the non-deterministic check, gate the +deterministic ones), tuned so "we take security seriously" means *never ship a vulnerable build*, +not *block every contributor on outside-world drift*. + ### 5. Close the CI gaps **What.** Our CI currently runs only `cargo build`, `cargo build --features tray`, and From 98691a871d82146a2e59cf4d60bc5b6acc474f95 Mon Sep 17 00:00:00 2001 From: hellno Date: Sun, 21 Jun 2026 13:48:51 +0200 Subject: [PATCH 2/2] ci(supply-chain): block yanks at release, harden nightly alert (code-review on #82) Cross-model code review (Codex + subagents) on PR #122 found that applying yanked=warn uniformly let a release ship a yanked dependency, and the new audit.yml alert step had shell/robustness gaps. Adopt variant D2 and harden it. D2 (release boundary): deny.toml keeps `yanked = "deny"`; only the PR-time cargo-deny-advisories job downgrades yanks to a warning (`--warn yanked`, gated on pull_request). Releases, push-to-main, and the nightly audit still block yanks. Real RUSTSEC advisories still block everywhere. audit.yml alert hardening: - gate on `steps.deny.outcome == 'failure'` so a checkout/infra failure no longer files a misleading "advisory landed" issue - tolerate a transient `gh issue list` failure (|| true) so `set -e` can't swallow the alert - exact-title de-dup (post-filter; gh search is fuzzy) + a concurrency group so overlapping runs can't open duplicate trackers - pass `-R "$REPO"` explicitly to the gh calls Also: fix a stale deny.toml header that called the supply-chain gate "non-blocking" (it is required + blocking), annotate the ci.yml summary diagram, and stop the docs claiming a security yank always carries a RUSTSEC advisory. --- .github/workflows/audit.yml | 34 +++++++++++++++++++++++----------- .github/workflows/ci.yml | 15 ++++++++++----- deny.toml | 23 +++++++++++++---------- docs/AGENTIC-ENGINEERING.md | 34 +++++++++++++++++++--------------- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index af65560..d87b139 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -18,33 +18,45 @@ permissions: contents: read issues: write # so a failed scan can open/refresh a tracking issue (#82) +# One audit at a time: a manual workflow_dispatch overlapping the 07:00 cron must not race the +# search-then-create issue de-dup below into duplicate trackers. (#82) +concurrency: + group: supply-chain-audit + cancel-in-progress: false + jobs: advisories: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v2 + - id: deny + uses: EmbarkStudios/cargo-deny-action@v2 with: command: check advisories - # Make the daily watch LOUD (#82): a failed scheduled scan means a new RUSTSEC advisory landed - # (yanks are warn-level now, so this fires on real advisories, not yanks). A red scheduled run - # only emails repo admins by default — easy to miss — so open a tracking issue, or comment on - # the existing open one, to de-dup. Uses the preinstalled `gh` CLI: no third-party action added. + # Make the daily watch LOUD (#82): if the ADVISORY SCAN itself failed (the `steps.deny.outcome` + # guard, so a checkout / action-infra error does NOT masquerade as an advisory), a new RUSTSEC + # advisory or a yanked crate was found — the nightly keeps `yanked = "deny"` even though PRs only + # warn. A red scheduled run just emails admins by default, easy to miss, so open a tracking issue + # (or comment on the open one). `gh` is preinstalled: no third-party action added. - name: Open or refresh advisory tracking issue on failure - if: failure() + if: failure() && steps.deny.outcome == 'failure' env: GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} run: | set -euo pipefail title="Daily cargo-deny advisory scan is failing" run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" body=$(printf '%s\n\n%s\n\n%s' \ - "The scheduled \`cargo deny check advisories\` run failed (a new RustSec advisory likely landed)." \ + "The nightly \`cargo deny check advisories\` run failed — a new RustSec advisory or a yanked dependency was found. See the run for the crate and RUSTSEC id." \ "Failed run: ${run_url}" \ - "Triage in \`deny.toml\`: bump the dependency, or add a justified \`ignore\` entry. Close this issue once the daily scan is green again.") - existing=$(gh issue list --state open --search "${title} in:title" --json number --jq '.[0].number // empty') + "Triage in \`deny.toml\`: bump the dependency, or add a justified \`ignore\` entry. Close this issue once the nightly scan is green again.") + # Tolerate a transient list failure (|| true) so a flaky API can't swallow the alert, and + # match the title EXACTLY (gh search is tokenized/fuzzy) so we never comment on a look-alike. + existing=$(gh issue list -R "$REPO" --state open --search "${title} in:title" \ + --json number,title --jq "map(select(.title == \"${title}\")) | .[0].number // empty" || true) if [ -n "${existing}" ]; then - gh issue comment "${existing}" --body "${body}" + gh issue comment "${existing}" -R "$REPO" --body "${body}" else - gh issue create --title "${title}" --body "${body}" + gh issue create -R "$REPO" --title "${title}" --body "${body}" fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d5bee7..5ca31c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ name: CI # # quick (fmt, ~1 min) ──┬── linux : full build + clippy + test --workspace (+ anvil/Foundry) # ├── macos : build the shipping binaries + lint the macOS-only tray path -# ├── cargo-deny-advisories : security advisories gate (BLOCKING) +# ├── cargo-deny-advisories : security advisories gate (BLOCKING; a bare yank only warns on PRs, #82) # └── cargo-deny-supply-chain : bans/licenses/sources gate (BLOCKING) # # The `quick` gate fails cheap on the most common trivial mistake (unformatted code) before the two @@ -111,10 +111,11 @@ jobs: # Supply-chain SECURITY gate — advisories only. BLOCKING on real RUSTSEC advisories. Config: # deny.toml. Every advisory the RustSec DB reports is either fixed or carries a written, justified # `ignore` in deny.toml; a new, untriaged advisory fails this check and blocks the merge until a - # human triages it. A bare crates.io *yank* is warn-level (deny.toml `yanked = "warn"`, #82) so - # outside-world drift can't block an unrelated PR — but a real advisory still does, here and at - # release. A daily re-scan against the committed Cargo.lock runs from .github/workflows/audit.yml - # (which also opens a tracking issue on failure). Rationale: docs/AGENTIC-ENGINEERING.md §4. + # human triages it. deny.toml sets `yanked = "deny"`; ON PULL REQUESTS ONLY we downgrade a bare + # crates.io yank to a warning (`--warn yanked`) so outside-world drift can't block an unrelated PR + # (#82). At release (workflow_call → event_name=push), on push to main, and in the nightly audit, + # yanks still block. A daily re-scan runs from .github/workflows/audit.yml (which opens a tracking + # issue on failure). Rationale: docs/AGENTIC-ENGINEERING.md §4. cargo-deny-advisories: needs: quick runs-on: ubuntu-latest @@ -123,6 +124,10 @@ jobs: - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check advisories + # PR-only yank downgrade (warn, not block). Empty elsewhere → deny.toml's `yanked = "deny"` + # stands, so release/push/nightly still block yanks. cargo-deny-action appends + # command-arguments after the command: `cargo-deny … check advisories --warn yanked`. (#82) + command-arguments: ${{ github.event_name == 'pull_request' && '--warn yanked' || '' }} # Supply-chain HYGIENE — bans / licenses / sources. BLOCKING. The license allow-list and bans # policy are now fully seeded (deny.toml): permissive licenses allowed, GPL-3.0 scoped per-crate to diff --git a/deny.toml b/deny.toml index 237f164..d8f6e06 100644 --- a/deny.toml +++ b/deny.toml @@ -2,9 +2,11 @@ # Run locally with: cargo install cargo-deny && cargo deny check (or `cargo deny check advisories`). # # CI splits this into two jobs (ci.yml): -# • cargo-deny-advisories — `check advisories` → BLOCKING. Every advisory is fixed or -# carries a justified `ignore` below. -# • cargo-deny-supply-chain — `check bans sources licenses` → non-blocking until seeded (issue #40). +# • cargo-deny-advisories — `check advisories` → BLOCKING on real advisories. Every advisory +# is fixed or carries a justified `ignore` +# below. On PRs a bare yank only warns; +# release + nightly still block yanks (#82). +# • cargo-deny-supply-chain — `check bans sources licenses` → BLOCKING (required PR check). # A daily `check advisories` re-scan runs from .github/workflows/audit.yml. [graph] @@ -19,16 +21,17 @@ all-features = true # RustSec advisory DB. Vulnerabilities and unmaintained crates are denied by default in cargo-deny # v2 — a real advisory blocks PRs AND releases until it is fixed or given a justified `ignore` below. # -# `yanked` is WARN, not deny (#82). A crates.io "yank" (a published version withdrawn, often for a -# non-security reason like an accidental publish or a semver slip) is non-deterministic outside-world -# drift: it can flip with no change on our side and was blocking UNRELATED PRs (PR #81 ate a -# `bitcoin_hashes` yank). Warn keeps the yank visible in logs without gating any PR; a yank that is -# also a real security pull still surfaces as a RUSTSEC advisory and still blocks. Add to `ignore` -# ONLY with a written justification. +# `yanked = "deny"` is the BASELINE: releases, pushes to main, and the nightly audit (audit.yml) all +# refuse a yanked crate. The PR-time `cargo-deny-advisories` job in ci.yml alone downgrades this to a +# warning (`--warn yanked`, gated on `pull_request`) so a crates.io "yank" — non-deterministic +# outside-world drift that can flip with no change on our side, like the `bitcoin_hashes` yank that +# blocked unrelated PR #81 — stays visible on the PR without gating it. We keep the release boundary +# strict (not assume "a security yank always carries a RUSTSEC advisory") because a security-motivated +# yank can PRECEDE its advisory. Add to `ignore` ONLY with a written justification. (#82) # # Every entry below is a TRANSITIVE advisory with NO in-tree fix; removing them is tracked in #40. # Re-evaluate the whole list on each `just bump-gpui` / Helios / alloy bump — drop any that resolve. -yanked = "warn" +yanked = "deny" ignore = [ # ── rustls-webpki 0.101 — TLS path-validation advisories, reachable only through the Helios # light client's jsonrpsee 0.19 → rustls 0.21 stack. rustls 0.21 caps webpki at 0.101, so diff --git a/docs/AGENTIC-ENGINEERING.md b/docs/AGENTIC-ENGINEERING.md index 42ed9a5..bd26237 100644 --- a/docs/AGENTIC-ENGINEERING.md +++ b/docs/AGENTIC-ENGINEERING.md @@ -142,7 +142,7 @@ disallowed licenses, and untrusted sources. New `deny.toml`: ```toml [advisories] -yanked = "warn" # see "Advisory policy" below (#82) — a bare yank shouldn't block unrelated PRs +yanked = "deny" # baseline; the PR-time job alone downgrades to warn — see "Advisory policy" below (#82) ignore = [] # add { id = "RUSTSEC-…", reason = "…" } only with written justification [licenses] @@ -187,22 +187,26 @@ duplicates crates. We therefore land the CI job **non-blocking** and promote it so they stay **required, blocking PR gates**. The advisory check is different: it re-reads the live RustSec database and crates.io yank status on every run, so a dependency yank or a freshly-published CVE can turn a PR red with **no change on our side**, blocking work that never touched that -dependency (this is what bit PR #81). We handle that by *kind*: - -- **A real RUSTSEC advisory still blocks** — on PRs, at release, and in the daily scan. A known - vulnerability stops the line until it is fixed or given a justified `ignore`. -- **A bare crates.io yank is warn-level** (`yanked = "warn"`). A yank is usually a pulled publish, - not a vulnerability; if it *is* a security pull it also carries a RUSTSEC advisory and still - blocks. So yanks stay visible without gating unrelated PRs. +dependency (this is what bit PR #81). We handle that by *kind*, and by *where*: + +- **A real RUSTSEC advisory always blocks** — on PRs, at release, on push to main, and in the daily + scan. A known vulnerability stops the line until it is fixed or given a justified `ignore`. +- **A bare crates.io yank blocks everywhere EXCEPT pull requests.** `deny.toml` keeps + `yanked = "deny"`; the PR-time `cargo-deny-advisories` job alone downgrades it to a warning + (`--warn yanked`, gated on `pull_request`), so a yank stays visible on the PR without blocking + unrelated work — while the release boundary, push-to-main, and the nightly audit still refuse a + yanked dependency. A yank is usually a pulled publish, not a vulnerability; but because a + security-motivated yank can *precede* its RustSec advisory, we keep the release boundary strict + rather than assume the advisory already exists. The safety net is three things, not the PR gate: continuous **daily detection** -(`.github/workflows/audit.yml`, which opens a tracking issue on failure so a new advisory is loud, -not just an email), a **hard gate at the release boundary** (the reusable `ci.yml` runs the advisory -check blocking when invoked from `release.yml`, so a known-vulnerable tree can never ship), and the -`ignore` list as the **deliberate, reviewed** escape hatch for transitive advisories with no in-tree -fix. This is the mainstream Rust-OSS posture (schedule the non-deterministic check, gate the -deterministic ones), tuned so "we take security seriously" means *never ship a vulnerable build*, -not *block every contributor on outside-world drift*. +(`.github/workflows/audit.yml`, which opens a tracking issue on failure so a new advisory or yank is +loud, not just an email), a **hard gate at the release boundary** (the reusable `ci.yml` runs the +advisory check — yanks included — blocking when invoked from `release.yml`, so a known-vulnerable or +yanked tree can never ship), and the `ignore` list as the **deliberate, reviewed** escape hatch for +transitive advisories with no in-tree fix. This is the mainstream Rust-OSS posture (schedule the +non-deterministic check, gate the deterministic ones), tuned so "we take security seriously" means +*never ship a vulnerable build*, not *block every contributor on outside-world drift*. ### 5. Close the CI gaps