diff --git a/.github/workflows/advisory-andon.yml b/.github/workflows/advisory-andon.yml new file mode 100644 index 0000000000..172a251d06 --- /dev/null +++ b/.github/workflows/advisory-andon.yml @@ -0,0 +1,44 @@ +name: advisory-andon +# PMAT-5792: an advisory already on main is an andon and a P0 issue within 4h; it never blocks a PR. +# Scheduled and push-to-main only -- never a PR or merge_group job (C324: no third state on that path). +on: + schedule: + - cron: '17 */2 * * *' + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read + issues: write +concurrency: + group: advisory-andon + cancel-in-progress: false +jobs: + andon: + runs-on: [self-hosted, Linux, X64, clean-room] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + - name: Install cargo-deny (if absent) + env: + CARGO_INSTALL_ROOT: ${{ runner.temp }}/cargo-deny + run: | + if ! command -v cargo-deny > /dev/null 2>&1; then + cargo install cargo-deny --locked + echo "$CARGO_INSTALL_ROOT/bin" >> "$GITHUB_PATH" + export PATH="$CARGO_INSTALL_ROOT/bin:$PATH" + fi + cargo deny --version + - name: Scan main and open or age the P0 issues + env: + GITHUB_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ github.token }} + run: | + export GIT_TERMINAL_PROMPT=0 GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.https://github.com/.extraheader + GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + export GIT_CONFIG_VALUE_0 + bash scripts/ci/advisory_andon.sh --self-test + out="${RUNNER_TEMP:?}/andon.ndjson" + rc=0 + cargo deny --locked --all-features --format json check advisories >/dev/null 2>"$out" || rc=$? + bash scripts/ci/advisory_andon.sh emit "$out" "$rc" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d3fa6f001..e9b4fdc76b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -322,6 +322,13 @@ jobs: contents: read steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Pin origin/main to this run's build base (PMAT-5792) + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} + run: bash scripts/ci/pin_build_base.sh - name: Install cargo-deny (if absent) env: CARGO_INSTALL_ROOT: ${{ runner.temp }}/cargo-deny @@ -335,11 +342,37 @@ jobs: - name: Advisories must pass, with deny.toml exemptions honoured env: GITHUB_TOKEN: ${{ github.token }} + MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} run: | export GIT_TERMINAL_PROMPT=0 GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.https://github.com/.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" export GIT_CONFIG_VALUE_0 - cargo deny check advisories + # The gate SCRIPTS run from the BASE ref copy (PMAT-5792); the workflow yaml and cargo config still come from the PR tree, so this is gate-content-reviewed, not self-judging-proof. + # Bootstrap only: the merge that introduces the scripts has no base copy, so it uses its own. + S="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/advgate-scripts-$$" + # merge_group: the base copy is taken only from the ref the merge queue named; a ref the PR tree re-pointed is refused, never used. + if [ -n "${MG_BASE_SHA:-}" ] && [ "$(git rev-parse -q --verify "refs/remotes/origin/main^{commit}" || true)" != "$MG_BASE_SHA" ]; then + echo "COULD-NOT-CHECK: refs/remotes/origin/main is not merge_group.base_sha $MG_BASE_SHA; the base gate is not taken from it" >&2 + exit 75 + fi + if git cat-file -e refs/remotes/origin/main:scripts/ci/advisory_gate.sh 2>/dev/null; then + mkdir -p "$S" + git archive refs/remotes/origin/main scripts/ci | tar -x -C "$S" + G="$S/scripts/ci" + else + G=scripts/ci + fi + # One command per line under -e: the first non-zero exit ends the step with that status (no && chain, no saved rc). + trap 'rm -rf -- "${S:?}"' EXIT + set -e + # The base gate runs FIRST: a PR-tree self-test below can write into $S, so none may run before it. + bash "$G/advisory_gate.sh" + bash "$G/advisory_diff.sh" --self-test + bash scripts/ci/check_sov_advisories_off.sh --self-test + bash scripts/ci/check_sov_advisories_off.sh + bash scripts/ci/advisory_gate_selftest.sh "$G/advisory_gate.sh" + bash scripts/ci/advisory_step_selftest.sh + bash scripts/ci/advisory_positive_control.sh - name: Every deny.toml exemption must still be live env: GITHUB_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 68106cff29..01c71588c7 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ publishing — all backed by YAML provable contracts that fail CI on drift. | Metric | Count | Source of truth | |-------:|------:|---| | Workspace crates | **81** workspace crates | `cargo metadata --no-deps` (NOT `ls crates/` — 4 are `exclude`d, 1 has no Cargo.toml) | -| Provable contracts | **1996** provable contracts | `contracts/census.json` `.n_files` — the set `pv lint` walks (`pv census`, ONT-001 ONT-1; regenerated by `make contracts`, written by `make readme-sync`, guarded by `scripts/check_readme_claims.sh`) | +| Provable contracts | **1997** provable contracts | `contracts/census.json` `.n_files` — the set `pv lint` walks (`pv census`, ONT-001 ONT-1; regenerated by `make contracts`, written by `make readme-sync`, guarded by `scripts/check_readme_claims.sh`) | | CLI commands | **113** CLI commands | `contracts/apr-cli-commands-v1.yaml` §`commands` (parse the list; `apr --help` also lists `help` itself, and `grep -c '^ - name:'` gives 119 — other same-indent `name:` keys exist in the file) | | Book CLI chapters | **114** chapters | `ls book/src/cli/*.md` | | Book lib chapters | **73** chapters | `ls book/src/lib/*.md` (parity with `pub mod`) | @@ -403,7 +403,7 @@ falsification_tests: prediction: apr validate bad-model.apr exits non-zero ``` -The tree carries 1996 contracts across inference, training, quantization, attention, FFN, +The tree carries 1997 contracts across inference, training, quantization, attention, FFN, tokenization, model formats, CLI safety — and this README itself. ## Migration from old crates diff --git a/ci/sections.yml b/ci/sections.yml index 4632770e37..d94387d4c8 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -18,12 +18,14 @@ # The old `ci:` job, verbatim: `uses:` is the pin ci/vendor/sovereign-ci.yml was # copied from (scripts/check_ci_reusable_workflow_pinned.sh checks both agree). sovereign-ci: - uses: paiml/.github/.github/workflows/sovereign-ci.yml@f713290c86fcd70d6f26a0faab14e67e6713586f + uses: paiml/.github/.github/workflows/sovereign-ci.yml@dbd0488efe2463161b34b2f75002baee5e308d2a with: repo: ${{ github.event.repository.name }} enable_sccache: true use_nextest: true coverage_on: 'tag' + # PMAT-5792: the guard-cargo diff gate is the scanner of record (check_sov_advisories_off.sh) + advisories_diff_only: true # A matrix job becomes one section per pinned combination. workspace-test-shard # runs as TWELVE shards (ONE count: the `shards:` pins below; ci.yml's matrix lists must match, check_shard_count_single_source.sh), each its own ci.yml job on its own runner: as one shard @@ -1843,6 +1845,7 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} + MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} run: bash scripts/ci_guards.sh guard-cargo guard-cargo-steps: @@ -1976,7 +1979,7 @@ jobs: # `ci / security` from the reusable workflow runs `cargo audit`, which does # NOT read deny.toml -- so this covers licences, bans and sources, which # nothing else does. - - name: cargo-deny (licences, bans, sources, advisories) + - name: cargo-deny (licences, bans, sources) env: GITHUB_TOKEN: ${{ github.token }} run: | @@ -1996,7 +1999,7 @@ jobs: -e GIT_CONFIG_KEY_0=http.https://github.com/.extraheader \ -e "GIT_CONFIG_VALUE_0=AUTHORIZATION: basic $B64" \ "$IMAGE" \ - cargo deny check + cargo deny check licenses bans sources # check_apr_bin_pinned asserts CALLERS pin the binary; it says nothing # about which binary the resolver hands them. apr_bin.sh returned # target/release/apr whenever it existed, so a stale release build beside @@ -2633,7 +2636,33 @@ jobs: export GIT_TERMINAL_PROMPT=0 GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.https://github.com/.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" export GIT_CONFIG_VALUE_0 - cargo deny check advisories + # MG_BASE_SHA comes from the process env of the outer guard-cargo step (the manifest runner resolves no merge_group context). + # The gate SCRIPTS run from the BASE ref copy (PMAT-5792); the workflow yaml and cargo config still come from the PR tree, so this is gate-content-reviewed, not self-judging-proof. + # Bootstrap only: the merge that introduces the scripts has no base copy, so it uses its own. + S="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/advgate-scripts-$$" + # merge_group: the base copy is taken only from the ref the merge queue named; a ref the PR tree re-pointed is refused, never used. + if [ -n "${MG_BASE_SHA:-}" ] && [ "$(git rev-parse -q --verify "refs/remotes/origin/main^{commit}" || true)" != "$MG_BASE_SHA" ]; then + echo "COULD-NOT-CHECK: refs/remotes/origin/main is not merge_group.base_sha $MG_BASE_SHA; the base gate is not taken from it" >&2 + exit 75 + fi + if git cat-file -e refs/remotes/origin/main:scripts/ci/advisory_gate.sh 2>/dev/null; then + mkdir -p "$S" + git archive refs/remotes/origin/main scripts/ci | tar -x -C "$S" + G="$S/scripts/ci" + else + G=scripts/ci + fi + # One command per line under -e: the first non-zero exit ends the step with that status (no && chain, no saved rc). + trap 'rm -rf -- "${S:?}"' EXIT + set -e + # The base gate runs FIRST: a PR-tree self-test below can write into $S, so none may run before it. + bash "$G/advisory_gate.sh" + bash "$G/advisory_diff.sh" --self-test + bash scripts/ci/check_sov_advisories_off.sh --self-test + bash scripts/ci/check_sov_advisories_off.sh + bash scripts/ci/advisory_gate_selftest.sh "$G/advisory_gate.sh" + bash scripts/ci/advisory_step_selftest.sh + bash scripts/ci/advisory_positive_control.sh # Separate from the check above on purpose: a newly-FIXED upstream must # never fail anyone's build. It fails only this guard, whose remedy is # deleting a line. diff --git a/ci/vendor/sovereign-ci.yml b/ci/vendor/sovereign-ci.yml index a5647865d8..f1fc95ad1f 100644 --- a/ci/vendor/sovereign-ci.yml +++ b/ci/vendor/sovereign-ci.yml @@ -1,5 +1,5 @@ -# VENDORED (#4433): paiml/.github .github/workflows/sovereign-ci.yml @ f713290c86fcd70d6f26a0faab14e67e6713586f -# sha256 of the upstream file: 7d93385d5f4327862e765302e9ea692e80388609563e83d0aa80e33f4497e502 +# VENDORED (#4433): paiml/.github .github/workflows/sovereign-ci.yml @ dbd0488efe2463161b34b2f75002baee5e308d2a +# sha256 of the upstream file: 40ab5f65bfe48630b475d124b1a492a7fcbaa4a9181b4af151889579ceb19c30 # Not a workflow here: scripts/ci/fat_driver.py runs its jobs as sections sov. of # the x86-main fat job. Do not edit by hand -- re-vendor from a new pin, and move the # pin in ci/sections.yml (sovereign-ci.uses) in the same commit. @@ -75,6 +75,14 @@ on: required: false default: 'always' type: string + advisories_diff_only: + description: >- + true: the caller judges advisories itself, as a diff against its base (aprender + PMAT-5792), so the plain `cargo deny check advisories` in `lint` and `cargo audit` + in `security` are skipped; licenses and sources still run. Default false: unchanged. + required: false + default: false + type: boolean run_benchmarks: description: 'Run criterion benchmarks (slow, use for nightly only)' required: false @@ -220,7 +228,7 @@ jobs: # uid:gid because the runner user has no entry in the image's passwd. chown -R 1000:1000 "$WORK_ROOT" 2>/dev/null || true echo "pre-checkout ownership restored under $WORK_ROOT" - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install extra packages @@ -360,16 +368,29 @@ jobs: # workspace-member lib tests are exercised. Default stays `--lib` (root only) for # back-compat: many repos have workspace members that don't build in the sovereign-ci # container. Opt-in callers pair this with `test_args` exclusions as needed. + # BIN-ONLY FALLBACK (2026-07-30) — the last link in each chain drops + # `--lib`. Every branch above passes it, and `cargo test --lib` does not + # merely fail on a crate without a [lib]: it ERRORS with "no library + # targets found in package " (exit 101). A bin-only repo could + # therefore never pass this job. Dropping `--lib` covers bins, + # integration tests and doctests, with or without a [lib]. + # + # Purely additive: a crate that HAS a lib target satisfies an earlier + # branch and never reaches these lines, so nothing changes for the repos + # that work today. if [ "$USE_NEXTEST" = "true" ]; then cargo nextest run $TEST_SCOPE $TEST_ARGS 2>&1 || \ cargo nextest run --lib -p "$REPO_NAME" $TEST_ARGS 2>&1 || \ + cargo nextest run $TEST_ARGS 2>&1 || \ { echo "::warning::nextest failed — falling back to cargo test"; \ cargo test $TEST_SCOPE $TEST_ARGS 2>&1 || \ cargo test --lib -p "$REPO_NAME" $TEST_ARGS 2>&1 || \ + cargo test $TEST_ARGS 2>&1 || \ { echo "::error::Tests failed — check workspace path dependencies"; exit 1; }; } else cargo test $TEST_SCOPE $TEST_ARGS 2>&1 || \ cargo test --lib -p "$REPO_NAME" $TEST_ARGS 2>&1 || \ + cargo test $TEST_ARGS 2>&1 || \ { echo "::error::Tests failed — check workspace path dependencies"; exit 1; } fi - name: Record sccache stats @@ -467,7 +488,7 @@ jobs: # uid:gid because the runner user has no entry in the image's passwd. chown -R 1000:1000 "$WORK_ROOT" 2>/dev/null || true echo "pre-checkout ownership restored under $WORK_ROOT" - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install extra packages @@ -614,6 +635,7 @@ jobs: # the stricter check. env: GITHUB_TOKEN: ${{ github.token }} + SOV_ADVISORIES_DIFF_ONLY: ${{ inputs.advisories_diff_only }} run: | # forjar#422: the advisory-db clone is an anonymous git-over-HTTPS # fetch of a public repo, and GitHub answers the fleet's IP with 401 @@ -631,7 +653,13 @@ jobs: GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" export GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 GIT_TERMINAL_PROMPT=0 if [ -f deny.toml ]; then - cargo deny check advisories licenses sources + # advisories_diff_only: true: the caller judges advisories as a diff against its + # base (aprender PMAT-5792), so a main-resident advisory must not stop its PRs. + if [ "${SOV_ADVISORIES_DIFF_ONLY:-}" = true ]; then + cargo deny check licenses sources + else + cargo deny check advisories licenses sources + fi else echo "::warning::No deny.toml — skipping supply chain audit" fi @@ -722,7 +750,7 @@ jobs: # uid:gid because the runner user has no entry in the image's passwd. chown -R 1000:1000 "$WORK_ROOT" 2>/dev/null || true echo "pre-checkout ownership restored under $WORK_ROOT" - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install extra packages @@ -854,6 +882,17 @@ jobs: # test job comment for back-compat rationale. cargo llvm-cov test $TEST_SCOPE --no-cfg-coverage --no-cfg-coverage-nightly --lcov --output-path lcov.info $TEST_ARGS 2>&1 || \ cargo llvm-cov test --lib --no-cfg-coverage --no-cfg-coverage-nightly -p "$REPO_NAME" --lcov --output-path lcov.info 2>&1 || \ + # BIN-ONLY FALLBACK (2026-07-30). Every branch above passes `--lib`, and + # `cargo test --lib` does not merely fail on a crate without a [lib] — it + # ERRORS: "no library targets found in package " (exit 101). So a + # bin-only repo could never produce coverage; cohete's job went red for + # this and nothing else. Dropping `--lib` covers bins, integration tests + # and doctests, and works with or without a [lib]. + # + # Purely additive: a crate that HAS a lib target satisfies one of the + # branches above and never reaches this line, so behaviour is unchanged + # for every repo that works today. + cargo llvm-cov test --no-cfg-coverage --no-cfg-coverage-nightly --lcov --output-path lcov.info 2>&1 || \ { echo "::error::Coverage failed — check workspace path dependencies"; exit 1; } - name: Enforce coverage floor (OPT-IN ratchet — PMAT build-system audit gap #1) # ZERO BEHAVIOR CHANGE GUARANTEE: @@ -1014,7 +1053,7 @@ jobs: # uid:gid because the runner user has no entry in the image's passwd. chown -R 1000:1000 "$WORK_ROOT" 2>/dev/null || true echo "pre-checkout ownership restored under $WORK_ROOT" - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install extra packages @@ -1152,7 +1191,7 @@ jobs: 2>/dev/null || echo "::warning::sccache stats unavailable" - name: Upload benchmark results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: bench-${{ inputs.repo }} path: bench-results.txt @@ -1241,7 +1280,7 @@ jobs: else echo "pre-checkout ownership restored under $WORK_ROOT" fi - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: roadmap-valid @@ -1472,6 +1511,7 @@ jobs: - name: Audit env: GITHUB_TOKEN: ${{ github.token }} + SOV_ADVISORIES_DIFF_ONLY: ${{ inputs.advisories_diff_only }} run: | # forjar#422: same throttle as the cargo-deny step — authenticate the # advisory-db fetch with the job's own token via a url rewrite. @@ -1512,7 +1552,11 @@ jobs: fi fi - cargo audit $IGNORE_FLAGS + if [ "${SOV_ADVISORIES_DIFF_ONLY:-}" = true ]; then + echo "cargo audit skipped: the caller's diff gate is the scanner of record (advisories_diff_only: true)" + else + cargo audit $IGNORE_FLAGS + fi # Symmetry with the container jobs (paiml/.github#42): every clean-room # job both tolerates poison on entry and refuses to leave any on exit, # so the invariant is uniform and greppable rather than per-job folklore. @@ -1568,7 +1612,7 @@ jobs: attestations: write contents: read steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Generate SLSA provenance diff --git a/contracts/advisory-gate-diff-v1.yaml b/contracts/advisory-gate-diff-v1.yaml new file mode 100644 index 0000000000..87be729e14 --- /dev/null +++ b/contracts/advisory-gate-diff-v1.yaml @@ -0,0 +1,127 @@ +metadata: + version: 1.0.0 + created: '2026-10-20' + author: PAIML Engineering + description: "Advisory gate: a PR fails only on advisories it introduces; an advisory already on main is an andon with a P0 4 h clock and does not block PRs; one scanner of record. Goal: 0 h total stop on a RustSec publication. DRAFT; gate-accept (one-way door), solo + non-author sign-off + operator yes before arming; any .github/workflows edit goes to a quorum first." + kind: pattern + references: + - "paiml/aprender#5792 (PMAT-5792) -- this ticket" + - "paiml/aprender#5791 -- cargo deny advisories is blind to RUSTSEC-2026-0378/0379 on main's lock; only cargo audit sees them" + - "paiml/aprender#5789 -- lock-only wasmtime bump that unblocked the queue" + - "MG run 39413691864 (#5781) -- P0 red on a publication, not a PR change" + +five_whys: + symptom: "Every PR in the merge queue went red when RUSTSEC-2026-0378/0379 were published against wasmtime 48.0.6 already on main." + why_1: "The security step fails on ANY advisory in the lock, not on advisories the PR adds." + why_2: "The gate compares the lock to the advisory DB, never to main's lock, so a new DB entry and a new dependency look identical." + why_3: "Two scanners (cargo deny, cargo audit) read different graphs (deny: all-features=false) and disagree; neither is the scanner of record, so the queue's red came from whichever saw it last." + why_4: "Nothing routes a main-resident advisory to a fix ticket with a clock; the only response available is to stop merging." + why_5: "Gate design treated 'the tree is clean' and 'this PR is safe to merge' as one question." + root_cause: "The merge decision is coupled to the global advisory state of main instead of the delta of the PR." + +equations: + pr_gate_verdict: + formula: "fail(PR) iff A(lock_PR, db) \\ A(lock_main, db) != {} ; A(lock, db) = findings keyed code|id|crate@version (yanked included) in db that hit a crate@version in lock, minus ignored ids" + domain: "lock_PR at head, lock_main at the PR merge base (merge_group base_sha), scanned from a base-commit worktree with the BASE deny.toml and scripts; db is ONE pinned snapshot for both sides" + codomain: "pass | fail" + invariants: + - "Both sides use the same advisory-db snapshot (otherwise a DB refresh between the runs reads as a PR-introduced advisory)" + - "A PR that does not touch Cargo.lock has an empty delta and passes" + - "A PR that removes an advisory-hit crate passes" + main_resident_advisory: + formula: "A(lock_main, db) != {} => andon raised AND P0 ticket with due <= 4h AND PR gate unaffected" + domain: "scheduled or push-to-main scan only (never the PR path: C324 rule 6, read-only token, GH-1); due is a label plus created_at" + codomain: "ticket id + due time, or none" + invariants: + - "The andon fires within one scan period of the DB publishing the advisory" + - "The ticket carries advisory ids, affected crate@version, and the clock start" + - "A main-resident advisory never turns the PR gate or the merge queue red" + one_scanner_of_record: + formula: "exactly one scanner decides A(); the other is retired or run as a LAB cross-check that cannot block" + domain: "cargo deny check advisories, cargo audit" + codomain: "the scanner name recorded in deny.toml / the gate script header" + invariants: + - "The scanner of record sees the whole Cargo.lock (all features), proven on the 48.0.6 lock as the positive control" + - "Exemptions live in one file" + - "Findings are keyed code|advisory-id|crate@version over every graph, so a new crate under an id main already carries, and a yanked crate with no id, are both PR-introduced" + - "Both sides are scanned with the BASE deny.toml; a PR that adds an advisories ignore id fails (ignores-added), so the one bypass through deny.toml is closed. This is NOT a claim that a PR cannot edit its own judge: the gate scripts, ci/sections.yml and cargo config (a .cargo alias can shadow cargo-deny) run from the PR tree, so a PR that edits them is judged by its own edit. Those paths are gate content and need the arbiter sign-off (non-author quorum); pinning the judge to main is out of this PR" + - "deny.toml advisories ignores are id-wide: cargo-deny has no per-crate ignore. They stay bounded, not scoped: ignores-added fails any PR that adds one, and check_deny_exemptions_live.sh (now --all-features, same as the gate) fails a dead one. Per-crate scoping needs an upstream cargo-deny feature and is out of this PR" + - "The base is pinned: merge_group uses merge_group.base_sha, pull_request uses pull_request.base.sha, anything else (or a non-sha) is COULD-NOT-CHECK rc 75, never a pass" + +proof_obligations: + - id: ADG-INV-001 + type: invariant + property: "A PR that adds no advisory passes while main holds one" + formal: "∀ pr: A(lock_PR) = A(lock_main) ∧ A(lock_main) ≠ ∅ ⇒ verdict = pass" + applies_to: pr_gate_verdict + - id: ADG-INV-002 + type: invariant + property: "A PR that introduces an advisory fails" + formal: "∀ pr: A(lock_PR) \\ A(lock_main) ≠ ∅ ⇒ verdict = fail" + applies_to: pr_gate_verdict + - id: ADG-INV-003 + type: invariant + property: "A main-resident advisory raises an andon and a <=4h P0 ticket" + formal: "∀ main: A(lock_main) ≠ ∅ ⇒ ticket(P0) ∧ due ≤ 4h" + applies_to: main_resident_advisory + - id: ADG-INV-004 + type: invariant + property: "The scanner of record is blind to nothing the other scanner sees" + formal: "∀ lock_48_0_6: A_record(lock) ∋ RUSTSEC-2026-0378 ∧ A_record(lock) ∋ RUSTSEC-2026-0379" + applies_to: one_scanner_of_record + +falsification_tests: + - id: FALSIFY-ADG-001 + rule: "Main-resident advisory does not block a PR" + prediction: "with lock_PR == lock_main (wasmtime 48.0.6) and a DB holding 0378/0379, the PR verdict is pass and a main-advisory andon row is emitted" + test: "planted-fixture case table of the diff gate script scripts/ci/advisory_diff.sh --self-test, 14 cases: same lock both sides" + if_fails: "the gate still compares lock to DB, not PR to main" + mutation: "compare lock_PR to the empty set; the case must turn RED" + - id: FALSIFY-ADG-002 + rule: "A PR-introduced advisory blocks" + prediction: "with lock_main = 48.0.7 and lock_PR = 48.0.6, the verdict is fail naming 0378 and 0379" + test: "same case table: lock_PR adds the vulnerable version" + if_fails: "the gate cannot turn RED on a real regression (a gate that cannot fail)" + mutation: "in scripts/ci/advisory_diff.sh verdict() subtract the PR keys from themselves instead of main keys (comm -23 PR PR); self-test cases (incl. an error finding with graphs:[] must still block; a mutant yielding no key for it turns RED) PR adds a new id, PR adds a crate under an id main has, and PR adds a yanked crate must turn RED (measured 2026-10-20)" + - id: FALSIFY-ADG-003 + rule: "Scanner of record sees the optional-feature crate" + prediction: "on the 48.0.6 lock the scanner of record exits non-zero and names 0378 and 0379" + test: "scripts/ci/advisory_gate_selftest.sh runs advisory_gate.sh against a stub cargo; every advisories scan must carry --all-features ; scripts/ci/advisory_positive_control.sh runs a REAL cargo deny --all-features on the committed wasmtime 48.0.6 lock fixture and fails unless 0378 and 0379 are both reported (the positive control that replaces cargo audit)" + if_fails: "#5791 is not resolved: the record scanner is as blind as deny with all-features=false" + mutation: "sed out --all-features from advisory_gate.sh scan(); the 'ADG-003' mutant case in advisory_gate_selftest.sh must turn RED (measured 2026-10-20)" + - id: FALSIFY-ADG-004 + rule: "Same DB snapshot on both sides" + prediction: "a DB entry published between the main run and the PR run does not appear in the delta" + test: "advisory_gate_selftest.sh: exactly one `cargo deny fetch`, both scans --offline (0.20) or --disable-fetch (older cargo-deny) with one --config; behavioural rows: a PR-introduced advisory makes the gate exit 1, a PR-added ignore exits 1, merge_group without base_sha exits 75, and replacing the verdict / ignores-added / base-ref call with `true` turns the matching row RED" + if_fails: "DB refreshes read as PR-introduced advisories and re-create the stop" + mutation: "sed out --offline, or add a second fetch before the base scan; the two 'ADG-004' mutant cases must turn RED (measured 2026-10-20)" + - id: FALSIFY-ADG-005 + rule: "Andon clock" + prediction: "the P0 ticket's due time is <= 4 h after the advisory first appears on main" + test: "case table over the ticket emitter" + if_fails: "main-resident advisories sit unowned" + mutation: "set the due time to 8 h; the case must turn RED" + - id: FALSIFY-ADG-006 + rule: "No second scanner blocks a PR on main's advisories" + prediction: "sov.lint and sov.security skip plain cargo deny advisories / cargo audit (advisories_diff_only: true in the vendored sovereign-ci)" + test: "scripts/ci/check_sov_advisories_off.sh --self-test, 4 cases, then the real tree" + if_fails: "the vendored scans still stop every PR on an advisory already on main (the #5793 REJECT)" + mutation: "set advisories_diff_only: false in ci/sections.yml, or change the vendored '= true' test; the matching case must turn RED (measured 2026-10-20)" + + - id: FALSIFY-ADG-007 + rule: "The CI step fails closed on the gate's exit status" + prediction: "the 'Advisories must pass' step of ci.yml exits with the gate's (or any earlier self-test's) non-zero status; a step without the gate line does not" + test: "scripts/ci/advisory_step_selftest.sh, 4 cases + 1 mutant: the step's run block is extracted from ci.yml and run against stub scripts on a throwaway origin/main; gate rc 75 gives 75, gate rc 1 gives 1, a failing self-test gives its rc, all green gives 0" + if_fails: "a red or could-not-check gate leaves the step green and a PR merges on an unjudged advisory" + mutation: "delete the advisory_gate.sh invocation from the step; the mutant case must stay green on a failing gate, which the cases above forbid (measured 2026-10-20)" + +qa_gate: + id: F-ADG-001 + name: Advisory gate diff contract + description: "PR delta gate, main andon, one scanner of record" + checks: + - pr_gate_verdict + - main_resident_advisory + - one_scanner_of_record + pass_criteria: "All 7 falsification tests pass; the scanner script has a must-fail/must-pass case table" + falsification: "Revert the gate to lock-vs-DB; FALSIFY-ADG-001 must turn RED" diff --git a/contracts/contracts.nt b/contracts/contracts.nt index 87447a2049..bb93209bb0 100644 --- a/contracts/contracts.nt +++ b/contracts/contracts.nt @@ -4691,6 +4691,11 @@ . "contracts/adamw-kernel-v1.yaml"^^ . "adamw-kernel-v1"^^ . + . + . + "contracts/advisory-gate-diff-v1.yaml"^^ . + "advisory-gate-diff-v1"^^ . + "pattern"^^ . . . . diff --git a/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json b/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json index 5ca9abb1de..daee4d60ee 100644 --- a/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json +++ b/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json @@ -1,5 +1,5 @@ { - "census_id_set_sha256": "db20faa02774eeffec52d05d3e579c0ec7047e2f0bed655de26b7e3eeff34151", + "census_id_set_sha256": "08a3fa4fb2b2814e04d0f19f101025396cf31cd9b20b3291402539b7edf94921", "relations_sha256": "811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850", "checkable_n": 87, "result": { diff --git a/docs/roadmaps/entries/PMAT-5792.yaml b/docs/roadmaps/entries/PMAT-5792.yaml new file mode 100644 index 0000000000..fd2d76887a --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5792.yaml @@ -0,0 +1,19 @@ +- id: PMAT-5792 + github_issue: 5792 + item_type: task + title: 'Advisory gate: PR fails only on advisories it introduces; main-resident advisory = andon, not a block (0h stop)' + status: planned + priority: critical + assigned_to: null + created: 2026-10-20T12:27:03Z + updated: 2026-10-20T12:27:03Z + spec: null + acceptance_criteria: + - 'Contract contracts/advisory-gate-diff-v1.yaml. Refs #5791. Gate-accept: solo + non-author sign-off + Noah''s yes; workflows edits go to quorum first.' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 3863 diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index fd9b5d5d2c..bcc89a34b6 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -25340,6 +25340,25 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 5510 +- id: PMAT-5792 + github_issue: 5792 + item_type: task + title: 'Advisory gate: PR fails only on advisories it introduces; main-resident advisory = andon, not a block (0h stop)' + status: planned + priority: critical + assigned_to: null + created: 2026-10-20T12:27:03Z + updated: 2026-10-20T12:27:03Z + spec: null + acceptance_criteria: + - 'Contract contracts/advisory-gate-diff-v1.yaml. Refs #5791. Gate-accept: solo + non-author sign-off + Noah''s yes; workflows edits go to quorum first.' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 3863 - id: PMAT-5817 github_issue: 5817 item_type: task diff --git a/scripts/check_deny_exemptions_live.sh b/scripts/check_deny_exemptions_live.sh index 71fcd21c04..adf7177427 100755 --- a/scripts/check_deny_exemptions_live.sh +++ b/scripts/check_deny_exemptions_live.sh @@ -35,7 +35,7 @@ trap 'rm -f "${LOG:?}"' EXIT # Redirect, never pipe: reading this through a pipe would report the exit status # of the last stage instead of cargo-deny's. -cargo deny check advisories > "$LOG" 2>&1 +cargo deny --all-features check advisories > "$LOG" 2>&1 rc=$? declared="$(grep -c 'id = "RUSTSEC' deny.toml || true)" @@ -64,7 +64,10 @@ if [ "$dead" -gt 0 ]; then exit 1 fi -if [ "$rc" -ne 0 ]; then +if [ "$rc" -ne 0 ] && grep -qE '^error\[(vulnerability|unmaintained|unsound|yanked|notice)\]' "$LOG"; then + # PMAT-5792: findings are advisory-diff territory (scripts/ci/advisory_gate.sh), not this guard's. + printf '\nNOTE: advisory findings present; judged by advisory_gate.sh, not here.\n' +elif [ "$rc" -ne 0 ]; then printf '\nFAIL: `cargo deny check advisories` exited %s. Its last lines:\n' "$rc" tail -n 8 "$LOG" | sed 's/^/ /' tail -20 "$LOG" | sed 's|^| |' diff --git a/scripts/ci/advisory_andon.sh b/scripts/ci/advisory_andon.sh new file mode 100644 index 0000000000..08dc7837fa --- /dev/null +++ b/scripts/ci/advisory_andon.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# advisory_andon.sh -- main-resident advisories are an andon plus a P0 ticket, never a PR block +# (PMAT-5792, contracts/advisory-gate-diff-v1.yaml main_resident_advisory). Runs ONLY on a +# schedule or push to main, never on a PR or merge_group path (C324: no third state there). +# +# advisory_andon.sh keys FILE finding keys of a cargo-deny NDJSON file (advisory_diff.sh keys) +# advisory_andon.sh due CREATED NOW exit 0 if the ticket is older than 4h (label p0-overdue), else 1 +# advisory_andon.sh emit FILE RC open one P0 issue per main-resident finding; label p0-overdue past 4h +# advisory_andon.sh --self-test +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DUE_SECS=$((4 * 3600)) + +due() { # ISO created, ISO now + local c n + c=$(date -u -d "$1" +%s) n=$(date -u -d "$2" +%s) # bashrs disable-line=DET002 (elapsed-time arithmetic, not a stored timestamp) + [ $((n - c)) -gt "$DUE_SECS" ] +} + +GH="${GH:-gh}" +emit() { # NDJSON-FILE cargo-deny-rc + case "$2" in 0 | 1) ;; *) echo "COULD-NOT-CHECK: cargo deny rc $2" >&2; return 75 ;; esac + local keys k found now + keys=$(bash "$HERE/advisory_diff.sh" checked-keys "$1" "$2") || return 75 + now=$(date -u +%Y-%m-%dT%H:%M:%SZ) + "$GH" label create advisory-andon --color B60205 2>/dev/null || true + "$GH" label create P0 --color B60205 2>/dev/null || true + "$GH" label create p0-overdue --color B60205 2>/dev/null || true + while IFS= read -r k; do + [ -n "$k" ] || continue + found=$("$GH" issue list --label advisory-andon --state open --limit 200 --json number,createdAt,labels,title \ + | jq -r --arg t "ADVISORY ANDON: $k" 'map(select(.title == $t)) | .[0] + | if . == null then "" else "\(.number) \(.createdAt) \([.labels[].name] | index("p0-overdue") != null)" end') || return 75 + if [ -z "$found" ]; then + "$GH" issue create --label advisory-andon --label P0 --title "ADVISORY ANDON: $k" \ + --body "Advisory $k is on main. It does not block PRs (PMAT-5792); fix within 4h of this issue's creation." || return 75 + else + set -- $found + if due "$2" "$now" && [ "$3" = false ]; then + "$GH" issue edit "$1" --add-label p0-overdue || return 75 + "$GH" issue comment "$1" --body "Open past the 4h P0 window." || return 75 + fi + fi + done <<<"$keys" +} + +self_test() { + local bad=0 rc + chk() { rc=0; due "$2" "$3" || rc=$?; [ "$rc" -eq "$4" ] || { echo "SELF-TEST FAIL: $1 want $4 got $rc" >&2; bad=1; }; } + chk 'under 4h is not due' 2026-10-20T10:00:00Z 2026-10-20T13:59:00Z 1 + chk 'exactly 4h is not due' 2026-10-20T10:00:00Z 2026-10-20T14:00:00Z 1 + chk 'over 4h is due' 2026-10-20T10:00:00Z 2026-10-20T14:00:01Z 0 + local d; d=$(mktemp -d) + # stub gh: logs argv; `issue list` answers from $STUB_LIST + printf '#!/usr/bin/env bash\necho "$*" >>"%s/calls"\n[ "$1 $2" = "issue list" ] && printf "%%s" "${STUB_LIST:-[]}"\nexit 0\n' "$d" >"$d/gh"; chmod +x "$d/gh" + printf '{"type":"diagnostic","fields":{"code":"vulnerability","severity":"error","advisory":{"id":"RUSTSEC-1"},"graphs":[{"Krate":{"name":"c","version":"1"}}]}}\n' >"$d/f.json" + ec() { # name want-rc want-grep(or -) rc list + : >"$d/calls"; rc=0; GH="$d/gh" STUB_LIST="$5" emit "$d/f.json" "$4" >/dev/null 2>&1 || rc=$? + [ "$rc" -eq "$2" ] || { echo "SELF-TEST FAIL: $1 rc want $2 got $rc" >&2; bad=1; } + if [ "$3" != - ]; then grep -q -- "$3" "$d/calls" || { echo "SELF-TEST FAIL: $1 missing call $3" >&2; bad=1; }; fi + } + ec 'new finding opens a P0 issue' 0 'issue create' 1 '' + ec 'a different open issue does not match' 0 'issue create' 1 '[{"number":9,"createdAt":"2020-01-01T00:00:00Z","labels":[],"title":"ADVISORY ANDON: other"}]' + : >"$d/empty.json"; rc=0; GH="$d/gh" emit "$d/empty.json" 1 >/dev/null 2>&1 || rc=$? + [ "$rc" -eq 75 ] || { echo "SELF-TEST FAIL: rc1 with no findings must be 75, got $rc" >&2; bad=1; } + ec 'tool error is cannot-check' 75 - 2 '' + ec 'old open issue gets p0-overdue' 0 'add-label p0-overdue' 1 '[{"number":7,"createdAt":"2020-01-01T00:00:00Z","labels":[],"title":"ADVISORY ANDON: vulnerability|RUSTSEC-1|c@1"}]' + ec 'fresh open issue is left alone' 0 - 1 "[{\"number\":7,\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"labels\":[],\"title\":\"ADVISORY ANDON: vulnerability|RUSTSEC-1|c@1\"}]" # bashrs disable-line=DET002 + : >"$d/calls"; GH="$d/gh" STUB_LIST="[{\"number\":7,\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"labels\":[],\"title\":\"ADVISORY ANDON: vulnerability|RUSTSEC-1|c@1\"}]" emit "$d/f.json" 1 >/dev/null 2>&1 || true # bashrs disable-line=DET002 + if grep -q 'issue create\|add-label' "$d/calls"; then echo "SELF-TEST FAIL: fresh issue was touched" >&2; bad=1; fi + rm -rf -- "${d:?}" + [ "$bad" -eq 0 ] && echo "advisory_andon self-test: ok" + return "$bad" +} + +case "${1:-}" in + keys) bash "$HERE/advisory_diff.sh" keys "$2" ;; + due) due "$2" "$3" ;; + emit) emit "$2" "$3" ;; + --self-test) self_test ;; + *) echo "usage: $0 keys FILE | due CREATED NOW | emit FILE RC | --self-test" >&2; exit 2 ;; +esac diff --git a/scripts/ci/advisory_diff.sh b/scripts/ci/advisory_diff.sh new file mode 100755 index 0000000000..20a7643bd6 --- /dev/null +++ b/scripts/ci/advisory_diff.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# advisory_diff.sh -- PR advisory verdict = findings the PR introduces (PMAT-5792, contract advisory-gate-diff-v1). +# +# advisory_diff.sh keys FILE print sorted finding keys "code|id|crate@version" from cargo-deny NDJSON +# advisory_diff.sh verdict PR_JSON PR_RC MAIN_JSON MAIN_RC +# exit 0: nothing new; 1: PR introduces findings; 75: could not check +# advisory_diff.sh --self-test case table +# +# Fail closed (exit 75, never a pass): rc 0 with findings, rc 1 with none, rc other than 0/1, unparsable JSON, +# a missing file. The key is code|advisory-id|crate@version so a new crate under an id main already carries, +# and a no-id finding (yanked at error severity, as deny.toml sets it), are both seen. cargo-deny writes NDJSON diagnostics to STDERR; the caller +# redirects it to FILE. +set -euo pipefail + +CANT=75 + +keys() { # FILE -> keys on stdout (use side/checked-keys to fail closed on unparsable input) + jq -R -r 'fromjson? | select(.type == "diagnostic" or has("fields")) + | .fields | select(.severity == "error") + | (.code // "?") as $c | (.advisory.id // "-") as $i | (((.graphs // []) | if length == 0 then [{}] else . end)[] | .Krate? // {}) | "\($c)|\($i)|\(.name // "?")@\(.version // "?")"' "$1" | LC_ALL=C sort -u +} + +parses() { # every non-blank line is JSON + jq -R -n -e '[inputs | select(length > 0) | fromjson] | length >= 0' "$1" >/dev/null 2>&1 +} + +side() { # JSON RC -> echoes keys; returns CANT when the side cannot be trusted + local f=$1 rc=$2 k + [ -f "$f" ] || { echo "COULD-NOT-CHECK: $f missing" >&2; return "$CANT"; } + case "$rc" in 0 | 1) ;; *) echo "COULD-NOT-CHECK: cargo-deny rc=$rc" >&2; return "$CANT" ;; esac + parses "$f" || { echo "COULD-NOT-CHECK: $f is not NDJSON" >&2; return "$CANT"; } + k=$(keys "$f") + local e; e=$(jq -R -r 'fromjson? | .fields? | select(.severity == "error") | .code' "$f" | head -c1) + if [ "$rc" -eq 1 ] && [ -z "$e" ]; then + echo "COULD-NOT-CHECK: rc=1 but no findings parsed; cargo-deny stderr (first 20 lines of $f):" >&2 + head -n 20 "$f" | cut -c1-300 >&2 + return "$CANT" + fi + if [ "$rc" -eq 0 ] && [ -n "$e" ]; then echo "COULD-NOT-CHECK: rc=0 but error findings parsed" >&2; return "$CANT"; fi + printf '%s\n' "$k" +} + +base_ref() { # EVENT MG_BASE_SHA PR_BASE_SHA -> the pinned base sha; 75 when it cannot be pinned + local ev=$1 mg=${2:-} pr=${3:-} + case "$ev" in + merge_group) [[ "$mg" =~ ^[0-9a-f]{40}$ ]] || { echo "COULD-NOT-CHECK: merge_group base_sha missing" >&2; return "$CANT"; }; echo "$mg" ;; + pull_request) [[ "$pr" =~ ^[0-9a-f]{40}$ ]] || { echo "COULD-NOT-CHECK: pull_request base sha missing" >&2; return "$CANT"; }; echo "$pr" ;; + *) echo "COULD-NOT-CHECK: event $ev has no merge base (push-to-main runs the full check)" >&2; return "$CANT" ;; + esac +} + +ignores_added() { # BASE_deny.toml PR_deny.toml -> ids the PR adds to [advisories] ignore; exit 1 when any + local add + add=$(comm -13 <(grep -o "RUSTSEC-[0-9]*-[0-9]*" "$1" | sort -u) <(grep -o "RUSTSEC-[0-9]*-[0-9]*" "$2" | sort -u)) + [ -z "$add" ] || { echo "PR widens exemptions:" >&2; printf "%s\n" "$add" >&2; return 1; } +} + +verdict() { + local pr main new + pr=$(side "$1" "$2") || return "$CANT" + main=$(side "$3" "$4") || return "$CANT" + new=$(comm -23 <(printf '%s\n' "$pr" | sed '/^$/d') <(printf '%s\n' "$main" | sed '/^$/d')) + if [ -n "$new" ]; then + echo "PR introduces advisories:" >&2 + printf '%s\n' "$new" >&2 + return 1 + fi + if [ -n "$(printf '%s' "$main" | tr -d '\n')" ]; then + echo "ANDON: advisories already on main (not blocking this PR): $(printf '%s' "$main" | tr '\n' ' ')" >&2 + fi + return 0 +} + +self_test() { + local d bad=0 + d=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf -- '${d:?}'" RETURN + row() { printf '{"type":"diagnostic","fields":{"code":"%s","severity":"%s","advisory":{"id":"%s"},"graphs":[{"Krate":{"name":"%s","version":"%s"}}]}}\n' "$@"; } + row vulnerability error RUSTSEC-2026-0378 wasmtime 48.0.6 >"$d/a.json" + row vulnerability error RUSTSEC-2026-0378 wasmtime 48.0.6 >"$d/a2.json"; row vulnerability error RUSTSEC-2026-0379 wasmtime-x 48.0.6 >>"$d/a2.json" + row vulnerability error RUSTSEC-2026-0378 wasmtime 48.0.6 >"$d/a3.json"; row vulnerability error RUSTSEC-2026-0378 wasmtime-y 1.0.0 >>"$d/a3.json" + : >"$d/empty.json" + printf 'garbage\n' >"$d/bad.json" + printf '{"type":"diagnostic","fields":{"code":"yanked","severity":"warning","graphs":[{"Krate":{"name":"y","version":"1"}}]}}\n' >"$d/yank.json" + t() { # name want pr-file pr-rc main-file main-rc + local rc=0 + verdict "$d/$3" "$4" "$d/$5" "$6" >/dev/null 2>&1 || rc=$? + [ "$rc" -eq "$2" ] || { echo "SELF-TEST FAIL: $1 wanted rc=$2 got $rc" >&2; bad=1; } + } + t 'same advisory on both sides: pass' 0 a.json 1 a.json 1 + t 'both clean: pass' 0 empty.json 0 empty.json 0 + t 'PR adds a new id: block' 1 a2.json 1 a.json 1 + t 'PR adds a crate under an id main has: block' 1 a3.json 1 a.json 1 + t 'PR adds a yanked crate at warn (deny.toml default): pass' 0 yank.json 0 empty.json 0 + printf '{"type":"diagnostic","fields":{"code":"yanked","severity":"error","graphs":[{"Krate":{"name":"y","version":"1"}}]}}\n' >"$d/yanke.json" + t 'PR adds a yanked crate at error: block' 1 yanke.json 1 empty.json 0 + t 'main has it, PR fixed it: pass' 0 empty.json 0 a.json 1 + t 'rc 0 but findings parsed: cannot check' 75 a.json 0 empty.json 0 + t 'rc 1 but nothing parsed: cannot check' 75 empty.json 1 empty.json 0 + t 'tool error rc 2: cannot check' 75 empty.json 2 empty.json 0 + t 'garbage JSON: cannot check' 75 bad.json 1 empty.json 0 + t 'main side missing: cannot check' 75 a.json 1 nope.json 1 + printf '{"type":"diagnostic","fields":{"code":"vulnerability","severity":"error","advisory":{"id":"RUSTSEC-2026-0999"},"graphs":[]}}\n' >"$d/nog.json" + t 'error finding with empty graphs[] is still a finding: block' 1 nog.json 1 empty.json 0 + t 'same empty-graphs finding on both sides: pass' 0 nog.json 1 nog.json 1 + sha=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + r() { local w=$1 rc=0; shift; base_ref "$@" >/dev/null 2>&1 || rc=$?; [ "$rc" -eq "$w" ] || { echo "SELF-TEST FAIL: base_ref $* wanted $w got $rc" >&2; bad=1; }; } + r 0 merge_group "$sha" ""; r 75 merge_group "" "$sha"; r 75 merge_group "main" ""; r 0 pull_request "" "$sha"; r 75 push "$sha" "$sha" + printf "ignore = [\"RUSTSEC-2026-0001\"]\n" >"$d/b.toml"; cp "$d/b.toml" "$d/same.toml"; printf "ignore = [\"RUSTSEC-2026-0001\", \"RUSTSEC-2026-0378\"]\n" >"$d/wide.toml" + ignores_added "$d/b.toml" "$d/same.toml" 2>/dev/null || { echo "SELF-TEST FAIL: same ignores must pass" >&2; bad=1; } + ! ignores_added "$d/b.toml" "$d/wide.toml" 2>/dev/null || { echo "SELF-TEST FAIL: added ignore must block" >&2; bad=1; } + [ "$bad" -eq 0 ] && echo "advisory_diff self-test: (14 verdict + 5 base_ref + 2 ignores) ok" + return "$bad" +} + +case "${1:-}" in + --self-test) self_test ;; + keys) keys "$2" ;; + checked-keys) side "$2" "$3" ;; + base-ref) base_ref "$2" "${3:-}" "${4:-}" ;; + ignores-added) ignores_added "$2" "$3" ;; + verdict) verdict "$2" "$3" "$4" "$5" ;; + *) echo "usage: $0 keys F | verdict PR PRRC MAIN MAINRC | --self-test" >&2; exit 2 ;; +esac diff --git a/scripts/ci/advisory_gate.sh b/scripts/ci/advisory_gate.sh new file mode 100755 index 0000000000..b898dd74cd --- /dev/null +++ b/scripts/ci/advisory_gate.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# advisory_gate.sh -- the advisories step of guard-cargo (PMAT-5792, contract advisory-gate-diff-v1). +# +# pull_request / merge_group fail only on advisories the PR INTRODUCES against the pinned build base +# (refs/remotes/origin/main, pinned by pin_build_base.sh); an advisory +# already on the base is reported, never a block. +# any other event (push) the full `cargo deny check advisories`, as before: a red there is the +# main-resident andon, not a PR block. +# Fails closed: anything that cannot be checked exits 75 and fails the step. Both sides are scanned with the +# BASE deny.toml, from the BASE worktree's script, over ONE advisory-db fetched once by this job. +set -euo pipefail + +HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +EV="${GITHUB_EVENT_NAME:-}" +case "$EV" in + pull_request | merge_group) ;; + *) exec cargo deny --all-features check advisories ;; +esac + +base_sha() { # the build base the pin step named + git rev-parse --verify -q refs/remotes/origin/main^{commit} || { echo "COULD-NOT-CHECK: origin/main is not pinned" >&2; return 75; } +} +BASE=$(base_sha) || exit 75 +# merge_group: the pin must equal merge_group.base_sha; a drifted pin is refused, not guessed around. +if [ "$EV" = merge_group ] && [ -n "${MG_BASE_SHA:-}" ] && [ "$BASE" != "$MG_BASE_SHA" ]; then + echo "COULD-NOT-CHECK: pinned base $BASE != merge_group base_sha $MG_BASE_SHA" >&2 + exit 75 +fi +bash "$HERE/advisory_diff.sh" base-ref "$EV" "${MG_BASE_SHA:-}" "$BASE" >/dev/null || exit 75 + +W="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/advgate-base-$$" +trap 'rm -f "${W:?}.pr.ndjson" "${W:?}.base.ndjson"; git worktree remove --force "${W:?}" >/dev/null 2>&1 || true' EXIT +git worktree add --detach "$W" "$BASE" >/dev/null 2>&1 || { echo "COULD-NOT-CHECK: no worktree for base $BASE (shallow fetch?)" >&2; exit 75; } + +# (before the subcommand) and replaced --disable-fetch with the global --offline, which `check` needs so both scans read the ONE advisory-db +# fetched above (without it 0.20 `check` may re-fetch the db and the two scans see different heads). --offline also makes cargo metadata need +# every crate in the registry cache (a cold runner exits rc=1 with log lines only, m70 #5793, measured 2026-10-21 on 0.20.2 with an empty +# CARGO_HOME), so `cargo fetch --locked` warms the cache for BOTH trees before either scan. Probe once, never assume the runner's version. +deny_help=$(cargo deny check --help 2>&1 || true) +if grep -qF -- '--config' <<<"$deny_help"; then DENY_OLD=1; else DENY_OLD=0; fi + +# One DB for both scans: fetched with the BASE config (the db-path both scans read), HEAD logged. +if [ "$DENY_OLD" = 1 ]; then cargo deny fetch --config "$W/deny.toml" db; else cargo deny --config "$W/deny.toml" fetch db; fi \ + || { echo "COULD-NOT-CHECK: advisory-db fetch failed" >&2; exit 75; } +for d in "$HOME"/.cargo/advisory-dbs/*; do + [ -d "$d/.git" ] && echo "advisory-db $(basename "$d") HEAD $(git -C "$d" rev-parse --short HEAD 2>/dev/null) $(git -C "$d" log -1 --format=%cI 2>/dev/null)" +done + +# The BASE script when it has one; the PR's copy only to bootstrap the merge that introduces the script. +GATE="$W/scripts/ci/advisory_diff.sh" +[ -f "$GATE" ] || GATE="$HERE/advisory_diff.sh" + +bash "$GATE" ignores-added "$W/deny.toml" deny.toml || exit 1 + +scan() { # TREE OUT.ndjson -> prints the cargo-deny rc + local rc=0 + if [ "$DENY_OLD" = 1 ]; then + (cd "$1" && cargo deny --locked --all-features --format json check --config "$W/deny.toml" --disable-fetch advisories) \ + >/dev/null 2>"$2" || rc=$? + else + (cd "$1" && cargo deny --config "$W/deny.toml" --locked --offline --all-features --format json check advisories) \ + >/dev/null 2>"$2" || rc=$? + fi + echo "$rc" +} +# warm the registry cache for both trees (network allowed here, nothing else reads it); the scans then run offline +for t in "$PWD" "$W"; do (cd "$t" && cargo fetch --locked) >/dev/null 2>&1 || { echo "COULD-NOT-CHECK: cargo fetch --locked failed in $t" >&2; exit 75; }; done +PRRC=$(scan "$PWD" "$W.pr.ndjson") +BASERC=$(scan "$W" "$W.base.ndjson") +bash "$GATE" verdict "$W.pr.ndjson" "$PRRC" "$W.base.ndjson" "$BASERC" diff --git a/scripts/ci/advisory_gate_selftest.sh b/scripts/ci/advisory_gate_selftest.sh new file mode 100644 index 0000000000..7c5ca451af --- /dev/null +++ b/scripts/ci/advisory_gate_selftest.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# advisory_gate_selftest.sh -- FALSIFY-ADG-003/004 (contracts/advisory-gate-diff-v1.yaml): run +# advisory_gate.sh against a stub `cargo` and read what it asked cargo to do. +# ADG-003 every advisories scan carries --all-features (the optional-feature crate is in scope) +# ADG-004 ONE advisory-db fetch; both scans read it (--disable-fetch on cargo-deny <= 0.19, --offline on 0.20) with the same --config +# usage: advisory_gate_selftest.sh [GATE_SCRIPT] (default: the sibling advisory_gate.sh) +set -euo pipefail + +HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +GATE_SRC="${1:-$HERE/advisory_gate.sh}" + +run_gate() { # prints the stub's argv log + local d repo log + d=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf -- '${d:?}'" RETURN + repo="$d/repo"; log="$d/cargo.log"; mkdir -p "$repo" "$d/bin" "$d/home" "$d/scripts" + cp "$GATE_SRC" "$d/scripts/advisory_gate.sh"; cp "$HERE/advisory_diff.sh" "$d/scripts/advisory_diff.sh" + # `cargo deny check --help` is the version probe: it is answered, never logged (STUB_OLD=1 plays cargo-deny <= 0.19) + printf '#!/usr/bin/env bash\ncase "$*" in *--help*) [ "${STUB_OLD:-0}" = 1 ] && echo -- --config; exit 0;; esac\necho "$*" >>"%s"\nexit 0\n' "$log" >"$d/bin/cargo"; chmod +x "$d/bin/cargo" + ( + cd "$repo" + git init -q . && printf '[advisories]\n' >deny.toml && git add deny.toml + # plumbing, not `git commit`: a throwaway repo must not run the global commit hooks + git update-ref refs/remotes/origin/main "$(git -c user.name=t -c user.email=t@t commit-tree -m base "$(git write-tree)")" + git update-ref HEAD refs/remotes/origin/main + PATH="$d/bin:$PATH" HOME="$d/home" TMPDIR="$d" GITHUB_EVENT_NAME=pull_request \ + bash "$d/scripts/advisory_gate.sh" >/dev/null 2>&1 + ) || { echo "gate-did-not-pass"; return 0; } + cat "$log" +} + +gate_rc() { # prints the gate's EXIT CODE under a scenario: STUB_PR_FIND=1 (the PR tree's scan reports an error finding, rc 1), + # WIDEN=1 (the PR adds an advisory id to deny.toml ignore), EV=, MG= + local d repo rc=0 + d=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf -- '${d:?}'" RETURN + repo="$d/repo"; mkdir -p "$repo" "$d/bin" "$d/home" "$d/scripts" + cp "$GATE_SRC" "$d/scripts/advisory_gate.sh"; cp "$HERE/advisory_diff.sh" "$d/scripts/advisory_diff.sh" + cat >"$d/bin/cargo" <<'STUB' +#!/usr/bin/env bash +case "$*" in *--help*) exit 0;; esac +case "$* " in + *" check "*) + if [ "${STUB_PR_FIND:-0}" = 1 ] && [[ "$PWD" != *advgate-base-* ]]; then + echo '{"type":"diagnostic","fields":{"code":"vulnerability","severity":"error","advisory":{"id":"RUSTSEC-2026-0378"},"graphs":[{"Krate":{"name":"wasmtime","version":"48.0.6"}}]}}' >&2 + exit 1 + fi ;; +esac +exit 0 +STUB + chmod +x "$d/bin/cargo" + ( + cd "$repo" + git init -q . && printf '[advisories]\n' >deny.toml && git add deny.toml + git update-ref refs/remotes/origin/main "$(git -c user.name=t -c user.email=t@t commit-tree -m base "$(git write-tree)")" + git update-ref HEAD refs/remotes/origin/main + if [ "${WIDEN:-0}" = 1 ]; then printf 'ignore = ["RUSTSEC-2026-0378"]\n' >>deny.toml; fi + PATH="$d/bin:$PATH" HOME="$d/home" TMPDIR="$d" GITHUB_EVENT_NAME="${EV:-pull_request}" MG_BASE_SHA="${MG:-}" \ + STUB_PR_FIND="${STUB_PR_FIND:-0}" bash "$d/scripts/advisory_gate.sh" >/dev/null 2>&1 + ) || rc=$? + echo "$rc" +} + +# scenario table: name, wanted exit code, env assignments. Each row also names the advisory_diff.sh call that decides it. +scenarios() { # prints the first defect, or ok + local got + got=$(gate_rc); [ "$got" = 0 ] || { echo "clean PR: gate exit $got, want 0"; return; } + got=$(STUB_PR_FIND=1 gate_rc); [ "$got" = 1 ] || { echo "verdict: PR-introduced advisory: gate exit $got, want 1"; return; } + got=$(WIDEN=1 gate_rc); [ "$got" = 1 ] || { echo "ignores-added: PR widens deny.toml ignore: gate exit $got, want 1"; return; } + got=$(EV=merge_group MG='' gate_rc); [ "$got" = 75 ] || { echo "base-ref: merge_group without base_sha: gate exit $got, want 75"; return; } + echo ok +} + +verdict_of() { # [old|new] (cargo-deny flavour the stub played); stdin: stub log -> "ok" or the first defect + local log fetch checks nall nfd ncfg + log=$(cat) + [ "$log" != gate-did-not-pass ] || { echo "gate did not pass against the stub"; return; } + fetch=$(grep -cE '^deny (--config [^ ]+ )?fetch' <<<"$log" || true) + checks=$(grep -c 'check' <<<"$(grep -vE '^deny (--config [^ ]+ )?fetch' <<<"$log")" || true) + nall=$(grep -c -- '--all-features' <<<"$(grep -vE '^deny (--config [^ ]+ )?fetch' <<<"$log")" || true) + nfd=$(grep -cE -- '--disable-fetch' <<<"$(grep -vE '^deny (--config [^ ]+ )?fetch' <<<"$log")" || true) + ncfg=$(grep -vE '^deny (--config [^ ]+ )?fetch' <<<"$log" | grep -o -- '--config [^ ]*' | sort -u | wc -l) + [ "$checks" -eq 2 ] || { echo "expected 2 scans, saw $checks"; return; } + [ "$nall" -eq 2 ] || { echo "ADG-003: $nall of 2 scans carry --all-features"; return; } + [ "$fetch" -eq 1 ] || { echo "ADG-004: $fetch advisory-db fetches, want 1"; return; } + if [ "${1:-new}" = old ]; then [ "$nfd" -eq 2 ] || { echo "ADG-004: $nfd of 2 scans carry --disable-fetch"; return; }; fi + # 0.20 (m70, #5793): both scans run --offline (one advisory-db head), and `cargo fetch --locked` runs on BOTH trees BEFORE the first scan, + # else --offline on a cold registry cache exits rc=1 with no diagnostics. The stub log is in call order. + if [ "${1:-new}" != old ]; then + [ "$(grep -c -- '--offline' <<<"$log")" -eq 2 ] || { echo "ADG-004: not both scans carry --offline"; return; } + [ "$(grep -c '^fetch --locked$' <<<"$log")" -eq 2 ] || { echo "ADG-004: cargo fetch --locked is not run once per tree"; return; } + [ "$(grep -n '^fetch --locked$' <<<"$log" | tail -1 | cut -d: -f1)" -lt "$(grep -n -- '--offline' <<<"$log" | head -1 | cut -d: -f1)" ] || { echo "ADG-004: cargo fetch --locked runs after a scan"; return; } + fi + [ "$ncfg" -eq 1 ] || { echo "ADG-004: scans use $ncfg different --config files"; return; } + echo ok +} + +main() { + local bad=0 d m got + d=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf -- '${d:?}'" EXIT + got=$(run_gate | verdict_of) + [ "$got" = ok ] || { echo "SELF-TEST FAIL: real gate: $got" >&2; bad=1; } + got=$(STUB_OLD=1 run_gate | verdict_of old) + [ "$got" = ok ] || { echo "SELF-TEST FAIL: real gate vs cargo-deny <= 0.19 flags: $got" >&2; bad=1; } + mutant() { # name sed-expr want-substring + sed -e "$2" "$GATE_SRC" >"$d/m.sh" + cmp -s "$d/m.sh" "$GATE_SRC" && { echo "SELF-TEST FAIL: mutant '$1' changed nothing" >&2; bad=1; return; } + m=$(STUB_OLD="${5:-0}" run_gate_with "$d/m.sh" | verdict_of "${4:-new}") + [[ "$m" == *"$3"* ]] || { echo "SELF-TEST FAIL: mutant '$1' stayed green: $m" >&2; bad=1; } + } + mutant 'scan without --all-features' 's/ --all-features//' 'ADG-003' + mutant 'scan reads the net (no --disable-fetch)' 's/ --disable-fetch//' 'ADG-004' old 1 + mutant 'scan not offline (0.20)' 's/--locked --offline --all-features/--locked --all-features/' 'ADG-004' + mutant 'no cargo fetch --locked before the scans (0.20)' 's/cargo fetch --locked)/true)/' 'ADG-004' + mutant 'second fetch before the base scan' 's|^BASERC=|cargo deny fetch --config "$W/deny.toml" db; BASERC=|' 'ADG-004' + # The gate's three advisory_diff.sh calls (base-ref, ignores-added, verdict) each decide a scenario: replaced by `true`, each must go red. + got=$(scenarios); [ "$got" = ok ] || { echo "SELF-TEST FAIL: real gate scenarios: $got" >&2; bad=1; } + smut() { # name sed-expr want-substring + sed -e "$2" "$GATE_SRC" >"$d/s.sh" + cmp -s "$d/s.sh" "$GATE_SRC" && { echo "SELF-TEST FAIL: mutant '$1' changed nothing" >&2; bad=1; return; } + m=$(GATE_SRC="$d/s.sh" scenarios) + [[ "$m" == *"$3"* ]] || { echo "SELF-TEST FAIL: mutant '$1' stayed green: $m" >&2; bad=1; } + } + smut 'verdict call replaced by true' 's|^bash "\$GATE" verdict .*|true|' 'verdict:' + smut 'ignores-added call replaced by true' 's|^bash "\$GATE" ignores-added .*|true|' 'ignores-added:' + smut 'base-ref call replaced by true' 's|^bash "\$HERE/advisory_diff.sh" base-ref .*|true|' 'base-ref:' + # ADG-003 on the other two scan sites: the push path and the exemption-liveness guard. + local f + for f in "$GATE_SRC" "${EXEMPT_SRC:-scripts/check_deny_exemptions_live.sh}"; do + [ -f "$f" ] || { echo "SELF-TEST FAIL: ADG-003: $f does not exist, so its scan sites cannot be judged" >&2; bad=1; continue; } + scans=$(grep -v 'printf' "$f" | grep -E '^[^#]*cargo deny .*check .*advisories' || true) + if [ -n "$scans" ] && grep -qv -- '--all-features' <<<"$scans"; then + echo "SELF-TEST FAIL: ADG-003: $f scans advisories without --all-features" >&2; bad=1 + fi + done + [ "$bad" -eq 0 ] && echo "advisory_gate self-test: 2 real (cargo-deny 0.19 and 0.20 flags) + 4 scenarios + 8 mutants + 2 sites ok" + return "$bad" +} + +run_gate_with() { GATE_SRC="$1"; run_gate; } +main diff --git a/scripts/ci/advisory_positive_control.sh b/scripts/ci/advisory_positive_control.sh new file mode 100644 index 0000000000..39786faa3d --- /dev/null +++ b/scripts/ci/advisory_positive_control.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# advisory_positive_control.sh -- the scanner of record must SEE RUSTSEC-2026-0378 and -0379 on the wasmtime 48.0.6 lock +# (PMAT-5792, ADG-INV-001 / FALSIFY-ADG-003). A real `cargo deny --all-features check advisories` runs against a committed +# lock fixture (fixtures/adv-positive-control/, stored as *.fixture so cargo does not treat the directory as a workspace +# member), with a deny.toml that carries no ignores. Exit 0 only when both ids are reported; cargo-deny missing or a +# scan that names neither id is a failure, never a skip. +set -euo pipefail + +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +fx="$here/fixtures/adv-positive-control" +d=$(mktemp -d) +trap 'rm -rf -- "${d:?}"' EXIT + +command -v cargo-deny >/dev/null || { echo "advisory_positive_control: cargo-deny is not installed -- the control cannot run" >&2; exit 1; } +mkdir -p "$d/src" +cp "$fx/Cargo.toml.fixture" "$d/Cargo.toml" +cp "$fx/Cargo.lock.fixture" "$d/Cargo.lock" +: >"$d/src/lib.rs" +printf '[advisories]\nversion = 2\n' >"$d/deny.toml" + +# Same probe as advisory_gate.sh: cargo-deny <= 0.19 takes --config after `check`, 0.20 takes it before the subcommand. +help=$(cargo deny check --help 2>&1 || true) +if grep -qF -- '--config' <<<"$help"; then + out=$(cd "$d" && cargo deny --locked --all-features --format json check --config "$d/deny.toml" advisories 2>&1) || true +else + out=$(cd "$d" && cargo deny --config "$d/deny.toml" --locked --all-features --format json check advisories 2>&1) || true +fi +for id in RUSTSEC-2026-0378 RUSTSEC-2026-0379; do + if ! grep -qF -- "$id" <<<"$out"; then + echo "advisory_positive_control: FAIL -- cargo deny on the wasmtime 48.0.6 lock did not report $id" >&2 + printf '%s\n' "$out" | tail -5 >&2 + exit 1 + fi +done +echo "advisory_positive_control: ok -- cargo deny reports RUSTSEC-2026-0378 and -0379 on the 48.0.6 lock" diff --git a/scripts/ci/advisory_step_selftest.sh b/scripts/ci/advisory_step_selftest.sh new file mode 100755 index 0000000000..e57b0b8219 --- /dev/null +++ b/scripts/ci/advisory_step_selftest.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# advisory_step_selftest.sh -- the CI step "Advisories must pass" must go RED when the gate (or any +# self-test before it) exits non-zero (PMAT-5792, ADG-008). Extracts the step's run block from +# .github/workflows/ci.yml, runs it in a throwaway repo whose origin/main holds stub scripts, and +# requires the step's exit status to equal the stub's. A mutant (gate line removed) must NOT be red. +# +# advisory_step_selftest.sh [WORKFLOW] WORKFLOW defaults to .github/workflows/ci.yml AND ci/sections.yml +set -euo pipefail + +wfs=("${1:-.github/workflows/ci.yml}") +[ -n "${1:-}" ] || wfs+=(ci/sections.yml) # the generated-from copy is held to the same rows +d=$(mktemp -d) +# shellcheck disable=SC2064 +trap "rm -rf -- '${d:?}'" EXIT + +extract() { # WORKFLOW -> the run block, de-indented + awk '/^ - name: Advisories must pass/ {on=1; next} + on && /^ - name:/ {exit} + on && /^ run: \|/ {run=1; next} + on && run {sub(/^ /, ""); print}' "$1" +} + +# stubs: each script exits with the status in .rc (default 0) +make_repo() { # DIR -> repo with scripts/ci stubs committed on refs/remotes/origin/main + local r=$1 f + mkdir -p "$r/scripts/ci" + for f in advisory_diff advisory_gate advisory_gate_selftest advisory_step_selftest advisory_positive_control check_sov_advisories_off; do + printf '#!/usr/bin/env bash\nexit "$(cat "$(dirname "$0")/%s.rc" 2>/dev/null || echo 0)"\n' "$f" >"$r/scripts/ci/$f.sh" + done + git -C "$r" init -q + git -C "$r" add -A + git -C "$r" -c user.name=t -c user.email=t@t write-tree >/dev/null + local tree c + tree=$(git -C "$r" write-tree) + c=$(git -C "$r" -c user.name=t -c user.email=t@t commit-tree "$tree" -m stub) + git -C "$r" update-ref refs/remotes/origin/main "$c" +} + +run_step() { # SCRIPTFILE REPO -> step exit status + local rc=0 + (cd "$2" && env -u RUNNER_TEMP TMPDIR="$d" GITHUB_TOKEN=x MG_BASE_SHA="${MG:-}" bash -c "$(cat "$1")" >/dev/null 2>&1) || rc=$? + echo "$rc" +} + +# $1: gate rc, $2: first self-test rc -- set in the BASE copy (what the step actually runs) and in the tree +# $6: "" | right | wrong -- MG_BASE_SHA the merge queue names: right = origin/main's sha, wrong = another sha +case_() { # name want gate_rc selftest_rc script [mg] + local name=$1 want=$2 got r + r=$(mktemp -d "$d/repo-XXXXXX") + make_repo "$r" + printf '%s' "$3" >"$r/scripts/ci/advisory_gate.rc" + printf '%s' "$4" >"$r/scripts/ci/advisory_diff.rc" + git -C "$r" add -A + local tree c + tree=$(git -C "$r" write-tree) + c=$(git -C "$r" -c user.name=t -c user.email=t@t commit-tree "$tree" -m stub2) + git -C "$r" update-ref refs/remotes/origin/main "$c" + case "${6:-}" in right) MG=$c ;; wrong) MG=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ;; *) MG= ;; esac + got=$(MG=$MG run_step "$5" "$r") + if [ "$got" -ne "$want" ]; then + echo "SELF-TEST FAIL: ${wf}: $name wanted step rc=$want, got $got" >&2 + bad=1 + fi +} + +bad=0 +for wf in "${wfs[@]}"; do + extract "$wf" >"$d/step.sh" + [ -s "$d/step.sh" ] || { echo "SELF-TEST FAIL: could not extract the 'Advisories must pass' step from $wf" >&2; exit 1; } + case_ 'gate rc 75 -> step RED with 75' 75 75 0 "$d/step.sh" + case_ 'gate rc 1 -> step RED with 1' 1 1 0 "$d/step.sh" + case_ 'advisory_diff self-test rc 3 -> step RED with 3' 3 0 3 "$d/step.sh" + case_ 'all green -> step green' 0 0 0 "$d/step.sh" + # the BASE gate runs before any PR-tree self-test: with both red, the gate's status wins + case_ 'gate runs before the PR-tree self-tests (gate 75, self-test 3 -> 75)' 75 75 3 "$d/step.sh" + # merge_group: origin/main must be the ref the queue named; the PR tree cannot re-point it + case_ 'merge_group with origin/main == base_sha -> green' 0 0 0 "$d/step.sh" right + case_ 'merge_group with origin/main != base_sha -> RED 75' 75 0 0 "$d/step.sh" wrong + # mutant: the gate invocation removed from the step; a failing gate must then (wrongly) pass -- proving the cases above bind to it + grep -v 'advisory_gate.sh"$' "$d/step.sh" >"$d/mut.sh" + cmp -s "$d/step.sh" "$d/mut.sh" && { echo "SELF-TEST FAIL: ${wf}: mutant did not change the step (gate line not found)" >&2; bad=1; } + case_ 'MUTANT (gate line removed) must NOT be red on a failing gate' 0 75 0 "$d/mut.sh" + # mutant 2: the gate moved to the END of the step (the shape the opus lane flagged): with both red, the self-test's 3 now wins + { grep -v 'advisory_gate.sh"$' "$d/step.sh"; grep 'advisory_gate.sh"$' "$d/step.sh"; } >"$d/mut2.sh" + case_ 'MUTANT (gate last) must lose to the self-test status' 3 75 3 "$d/mut2.sh" + # mutant 3: the merge_group base_sha guard removed (the whole if-block): a re-pointed origin/main must then (wrongly) pass + sed '/^if \[ -n "\${MG_BASE_SHA/,/^fi$/d' "$d/step.sh" >"$d/mut3.sh" + cmp -s "$d/step.sh" "$d/mut3.sh" && { echo "SELF-TEST FAIL: ${wf}: mutant 3 did not change the step (MG guard not found)" >&2; bad=1; } + case_ 'MUTANT (MG guard removed) must NOT be red on a wrong base_sha' 0 0 0 "$d/mut3.sh" wrong +done +if [ "$bad" -eq 0 ]; then + echo "advisory_step self-test: ${#wfs[@]} workflow(s) x (7 cases + 3 mutants) ok" +fi +exit "$bad" diff --git a/scripts/ci/check_sov_advisories_off.sh b/scripts/ci/check_sov_advisories_off.sh new file mode 100644 index 0000000000..9333c84682 --- /dev/null +++ b/scripts/ci/check_sov_advisories_off.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# check_sov_advisories_off.sh -- PMAT-5792: the vendored sov.lint `cargo deny check advisories` and +# sov.security `cargo audit` stop PRs on advisories already on main. They are off only when BOTH halves hold: +# 1. ci/sections.yml sovereign-ci.with sets advisories_diff_only: true +# 2. the vendored body reads that input in both steps and skips the scan on it +# usage: check_sov_advisories_off.sh [SECTIONS] [VENDORED] | --self-test +set -euo pipefail + +HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd -- "$HERE/../.." && pwd)" + +check() { # SECTIONS VENDORED -> 0 ok, 1 gap + local sec=$1 ven=$2 bad=0 + grep -Eq '^ advisories_diff_only: true$' "$sec" || { echo "GAP: $sec does not set advisories_diff_only: true under sovereign-ci.with" >&2; bad=1; } + [ "$(grep -c 'SOV_ADVISORIES_DIFF_ONLY: \${{ inputs.advisories_diff_only }}' "$ven")" -ge 2 ] \ + || { echo "GAP: $ven does not pass the input to both the deny and the audit step" >&2; bad=1; } + [ "$(grep -c '"${SOV_ADVISORIES_DIFF_ONLY:-}" = true' "$ven")" -ge 2 ] \ + || { echo "GAP: $ven does not skip both scans on the input" >&2; bad=1; } + return "$bad" +} + +self_test() { + local d bad=0 rc + d=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf -- '${d:?}'" RETURN + case_() { # name want sections vendored + rc=0; check "$3" "$4" >/dev/null 2>&1 || rc=$? + [ "$rc" -eq "$2" ] || { echo "SELF-TEST FAIL: $1 wanted rc=$2 got $rc" >&2; bad=1; } + } + S="$ROOT/ci/sections.yml"; V="$ROOT/ci/vendor/sovereign-ci.yml" + case_ 'real tree' 0 "$S" "$V" + sed 's/^ advisories_diff_only: true$/ advisories_diff_only: false/' "$S" >"$d/s.yml" + case_ 'MUTANT input off in sections' 1 "$d/s.yml" "$V" + sed 's/"${SOV_ADVISORIES_DIFF_ONLY:-}" = true/"${SOV_ADVISORIES_DIFF_ONLY:-}" = never/' "$V" >"$d/v.yml" + case_ 'MUTANT vendored body does not honour it' 1 "$S" "$d/v.yml" + sed '0,/SOV_ADVISORIES_DIFF_ONLY: ${{ inputs.advisories_diff_only }}/{//d}' "$V" >"$d/v2.yml" + case_ 'MUTANT one step loses the env' 1 "$S" "$d/v2.yml" + [ "$bad" -eq 0 ] && echo "check_sov_advisories_off self-test: 4 cases ok" + return "$bad" +} + +case "${1:-}" in + --self-test) self_test ;; + "") check "$ROOT/ci/sections.yml" "$ROOT/ci/vendor/sovereign-ci.yml" ;; + *) check "$1" "$2" ;; +esac diff --git a/scripts/ci/fixtures/adv-positive-control/Cargo.lock.fixture b/scripts/ci/fixtures/adv-positive-control/Cargo.lock.fixture new file mode 100644 index 0000000000..2dfb8cc42e --- /dev/null +++ b/scripts/ci/fixtures/adv-positive-control/Cargo.lock.fixture @@ -0,0 +1,459 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.135.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e38972e913f89b465f99d3a1add645d507ad8d2cb32b05161769330b72648c" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.135.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3352069928676bf94f502e352f4d879ba16dcd2fc413fddb9617d72eaf42127" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-entity" +version = "0.135.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce7ae3678365a2fced842f320dd90760816d57e62dc41e65804c7a86ce16dff2" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "pc" +version = "0.0.0" +dependencies = [ + "wasmtime", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "48.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da9e35352d5ed6636a3b18e2099f96cf00e9dcc707f7d9d599a64ec5fabcef7" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", +] + +[[package]] +name = "pulley-macros" +version = "48.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf76d5588cc67bac2ba972c751d4f1b4545eebf2dea8117410dc7f450bae6c97" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasmparser" +version = "0.254.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ff8685b587722894b6f25958fc608d315b93b8d23647bfb567664e7006d757" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "serde", +] + +[[package]] +name = "wasmtime" +version = "48.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a26206e1b2f1403d4d17c8fa7e736b693d7e9b1a7b9a5bf860e1c580472704d" +dependencies = [ + "bitflags", + "bumpalo", + "libc", + "log", + "object", + "postcard", + "pulley-interpreter", + "serde", + "serde_derive", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "48.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61f6e56877ffa9633e0d1ee3ee01c4b5dd08d8e1609417a245b496163526af" +dependencies = [ + "anyhow", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown", + "indexmap", + "log", + "object", + "postcard", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-core" +version = "48.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84493645ddd62c4615bbe0bd1a6bf9740e69c11b70404f2db8726ee793442b88" +dependencies = [ + "hashbrown", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "48.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1b5eb4b2838bbbeaadd02c61e307a1fdbd47efca9d96684c04de5721157dca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/scripts/ci/fixtures/adv-positive-control/Cargo.toml.fixture b/scripts/ci/fixtures/adv-positive-control/Cargo.toml.fixture new file mode 100644 index 0000000000..af5ae1fbb7 --- /dev/null +++ b/scripts/ci/fixtures/adv-positive-control/Cargo.toml.fixture @@ -0,0 +1,6 @@ +[package] +name="pc" +version="0.0.0" +edition="2021" +[dependencies] +wasmtime={version="=48.0.6",default-features=false} diff --git a/scripts/ci_mg_reuse.sh b/scripts/ci_mg_reuse.sh index 5f133ce19e..7bd4feee79 100755 --- a/scripts/ci_mg_reuse.sh +++ b/scripts/ci_mg_reuse.sh @@ -515,7 +515,7 @@ STUB if grep -qF 'outputs.mac' <<<"$(job mg-reuse)"; then fail=$((fail + 1)); echo "FAIL wiring: mg-reuse still exports mac, which no ci.yml job reads"; else pass=$((pass + 1)); fi wire x86-main-advisories "if: \${{ !cancelled() && needs.mg-reuse.outputs.x86 == '1' }}" - wire x86-main-advisories 'cargo deny check advisories' + wire x86-main-advisories 'advisory_gate.sh"' wire x86-main-advisories 'bash scripts/check_deny_exemptions_live.sh' wire ci-gate 'MG-REUSE-VERDICT-BEGIN' wire ci-gate 'needs: [x86-main, mg-reuse, x86-main-advisories]'