diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5c8b8ad..d87b139 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,12 +16,47 @@ on: 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): 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() && 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 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 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}" -R "$REPO" --body "${body}" + else + gh issue create -R "$REPO" --title "${title}" --body "${body}" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d7f97c..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 @@ -108,11 +108,14 @@ 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. 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 @@ -121,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 77214fb..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] @@ -17,7 +19,15 @@ 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 = "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. diff --git a/docs/AGENTIC-ENGINEERING.md b/docs/AGENTIC-ENGINEERING.md index 75a3e3c..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 = "deny" +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] @@ -182,6 +182,32 @@ 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*, 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 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 **What.** Our CI currently runs only `cargo build`, `cargo build --features tray`, and