From 067abb3ed99fcb1d083453327f948b1b454e74ad Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:58:46 +0000 Subject: [PATCH] fix(ci): validate bridge assessment before applying lowercase B1 verdicts --- .github/workflows/bridge-gate.yml | 21 +++++---- .machine_readable/descriptiles/STATE.a2ml | 5 +++ scripts/ci/check-bridge-report.sh | 40 +++++++++++++++++ scripts/ci/test-bridge-report.sh | 55 +++++++++++++++++++++++ 4 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 scripts/ci/check-bridge-report.sh create mode 100644 scripts/ci/test-bridge-report.sh diff --git a/.github/workflows/bridge-gate.yml b/.github/workflows/bridge-gate.yml index c52295eb..022b9099 100644 --- a/.github/workflows/bridge-gate.yml +++ b/.github/workflows/bridge-gate.yml @@ -26,7 +26,17 @@ on: permissions: contents: read jobs: + bridge-contract: + name: Bridge report contract + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7.0.1 + - name: Reject blocking, malformed and unavailable assessments + run: bash scripts/ci/test-bridge-report.sh + bridge-gate: + needs: bridge-contract if: ${{ startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'renovate/') }} runs-on: ubuntu-latest timeout-minutes: 25 @@ -51,12 +61,5 @@ jobs: - name: Bridge triage (queries OSV) -> BridgeReport run: panic-attack bridge triage . --output bridge-report.json - - name: Apply B1 verdict -- reachable unmitigable CVE -> flag - run: | - blockers=$(jq '[.cves[]? | select(.classification=="Unmitigable" and .reachability.status=="Reachable")] | length' bridge-report.json) - if [ "$blockers" -gt 0 ]; then - echo "::error::B1: $blockers reachable unmitigable CVE(s) -> safety=flag (auto-merge blocked)" - jq -r '.cves[]? | select(.classification=="Unmitigable" and .reachability.status=="Reachable") | " - \(.vulnerability.cve // .vulnerability.id): \(.rationale)"' bridge-report.json - exit 1 - fi - echo "B1 PASS: no reachable unmitigable CVEs -> arm-eligible (pool-gated)." + - name: Apply B1 verdict -- validate data and reject blockers + run: bash scripts/ci/check-bridge-report.sh bridge-report.json diff --git a/.machine_readable/descriptiles/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml index 86dfd8a9..6f676089 100644 --- a/.machine_readable/descriptiles/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -308,3 +308,8 @@ new-regression-source = "test/neural_source_store_lifecycle_test.exs" authored-new-test-functions = 22 qualification = "No execution receipt for this new lifecycle candidate. Existing runtime-path test receipts do not qualify it. Exact-source compile, focused and normal full gates remain required." authority = "No automatic restart, dispatch, or deployment promotion from a source refusal." + +[bridge-report-contract-2026-11-06] +status = "candidate; not merged or deployed" +change = "Validate schema 0.3.0 and vulnerability-data availability before interpreting native lowercase B1 verdicts" +validation = "29 local executable contract cases passed; workflow YAML and embedded shell parsed; upstream TOML dependency fix in panic-attack PR 265 still awaits qualified CI" diff --git a/scripts/ci/check-bridge-report.sh b/scripts/ci/check-bridge-report.sh new file mode 100644 index 00000000..157ef42f --- /dev/null +++ b/scripts/ci/check-bridge-report.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell +# Exit 0: qualified report has no B1 blockers; 1: blockers; 2: invalid/unavailable data. +set -euo pipefail +if [[ $# != 1 || ! -f "$1" ]]; then + echo '::error::B1 assessment unavailable: expected one report file.' >&2 + exit 2 +fi +# Read exactly one JSON document. jq's default stream handling could otherwise +# accept an empty file or let a second document overwrite the first verdict. +if ! blockers=$(jq -ers ' + def natural: type == "number" and . >= 0 and . == floor; + if length != 1 then error("expected one report") else .[0] end + | if ( + type == "object" + and .schema_version == "0.3.0" + and (.total_dependencies | natural) + and (.cves | type == "array") + and ( + (.vulnerability_data == "queried" and .total_dependencies > 0) + or (.vulnerability_data == "no-dependencies" and .total_dependencies == 0 and (.cves | length) == 0) + ) + and all(.cves[]; + type == "object" + and (.classification | IN("mitigable", "unmitigable", "informational")) + and (.reachability | type == "object") + and (.reachability.status | IN("reachable", "unreachable", "phantom-declared", "phantom-transitive")) + ) + ) then [.cves[] | select(.classification == "unmitigable" and .reachability.status == "reachable")] | length + else error("unsupported, incomplete or unavailable bridge assessment") end +' "$1"); then + echo '::error::B1 assessment unavailable: invalid report or missing vulnerability data.' >&2 + exit 2 +fi +if [[ "$blockers" -gt 0 ]]; then + printf '::error::B1: %s reachable unmitigable CVE(s); auto-merge blocked.\n' "$blockers" >&2 + exit 1 +fi +echo 'B1 PASS: qualified report contains no reachable unmitigable CVEs.' diff --git a/scripts/ci/test-bridge-report.sh b/scripts/ci/test-bridge-report.sh new file mode 100644 index 00000000..c09f736a --- /dev/null +++ b/scripts/ci/test-bridge-report.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell +set -euo pipefail +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +cases=0 +check() { + local expected=$1 label=$2 content=$3 actual=0 + printf '%s' "$content" > "$work/report.json" + bash "$script_dir/check-bridge-report.sh" "$work/report.json" > "$work/output" 2>&1 || actual=$? + if [[ "$actual" != "$expected" ]]; then + printf 'FAIL %s: expected exit %s, got %s\n' "$label" "$expected" "$actual" >&2 + cat "$work/output" >&2 + exit 1 + fi + cases=$((cases + 1)) +} +clean='{"schema_version":"0.3.0","vulnerability_data":"queried","total_dependencies":1,"cves":[]}' +finding='{"classification":"unmitigable","reachability":{"status":"reachable"}}' +blocked=$(jq --argjson finding "$finding" '.cves=[$finding]' <<< "$clean") +check 0 clean "$clean" +check 1 blocking "$blocked" +check 0 mitigable "$(jq '.cves[0].classification="mitigable"' <<< "$blocked")" +check 0 informational "$(jq '.cves[0].classification="informational" | .cves[0].reachability.status="phantom-declared"' <<< "$blocked")" +check 0 no-dependencies "$(jq '.vulnerability_data="no-dependencies" | .total_dependencies=0' <<< "$clean")" +check 2 offline "$(jq '.vulnerability_data="offline-unavailable"' <<< "$clean")" +check 2 missing-data "$(jq 'del(.vulnerability_data)' <<< "$clean")" +check 2 missing-cves "$(jq 'del(.cves)' <<< "$clean")" +check 2 null-cves "$(jq '.cves=null' <<< "$clean")" +check 2 object-cves "$(jq '.cves={}' <<< "$clean")" +check 2 unknown-schema "$(jq '.schema_version="0.4.0"' <<< "$clean")" +check 2 legacy-schema "$(jq 'del(.schema_version)' <<< "$clean")" +check 2 uppercase-verdict "$(jq '.cves[0].classification="Unmitigable"' <<< "$blocked")" +check 2 uppercase-reachability "$(jq '.cves[0].reachability.status="Reachable"' <<< "$blocked")" +check 2 missing-classification "$(jq 'del(.cves[0].classification)' <<< "$blocked")" +check 2 missing-reachability "$(jq 'del(.cves[0].reachability)' <<< "$blocked")" +check 2 null-finding "$(jq '.cves=[null]' <<< "$clean")" +check 2 bogus-empty "$(jq '.vulnerability_data="no-dependencies"' <<< "$blocked")" +check 2 queried-zero "$(jq '.total_dependencies=0' <<< "$clean")" +check 2 negative-count "$(jq '.total_dependencies=-1' <<< "$clean")" +check 2 fractional-count "$(jq '.total_dependencies=1.5' <<< "$clean")" +check 2 string-count "$(jq '.total_dependencies="1"' <<< "$clean")" +check 2 empty '' +check 2 malformed '{' +check 2 null null +check 2 array '[]' +check 2 multiple-documents "$blocked $clean" +check 2 multiple-clean-documents "$clean $clean" +actual=0 +bash "$script_dir/check-bridge-report.sh" "$work/missing.json" > /dev/null 2>&1 || actual=$? +[[ "$actual" == 2 ]] +cases=$((cases + 1)) +printf 'PASS: %s bridge report contract cases\n' "$cases"