Skip to content

Commit 0059776

Browse files
fix(ci): reject unavailable bridge data and interpret native CVE verdicts (#1017)
The B1 gate compares uppercase classification/reachability strings with panic-attack's lowercase wire values. A native reachable/unmitigable CVE is counted as zero blockers. Missing CVE arrays and unavailable assessment data can also pass the permissive filter. Validate schema 0.3.0, dependency counts, vulnerability-data availability and finding enums before interpreting a report. Exit 1 for B1 blockers and 2 for unavailable or invalid assessment; only qualified reports without blockers pass. Reject empty files and multiple JSON documents. Avoid emitting untrusted finding prose as workflow annotations. Add an independent contract job on every PR, with 29 executable cases, and require it before the existing bot dependency gate. Action references and permissions are unchanged. Validation: 29 local cases pass; changed YAML and all embedded shell parse. Reproduced the original false-negative with a native blocking report. Full remote workflows and live OSV triage remain pending; upstream compilation prerequisite is tracked in hyperpolymath/panic-attack#265. Related to #1009; does not close its broader fleet work.
2 parents 32a3659 + 067abb3 commit 0059776

4 files changed

Lines changed: 112 additions & 9 deletions

File tree

‎.github/workflows/bridge-gate.yml‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ on:
2626
permissions:
2727
contents: read
2828
jobs:
29+
bridge-contract:
30+
name: Bridge report contract
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 5
33+
steps:
34+
- uses: actions/checkout@v7.0.1
35+
- name: Reject blocking, malformed and unavailable assessments
36+
run: bash scripts/ci/test-bridge-report.sh
37+
2938
bridge-gate:
39+
needs: bridge-contract
3040
if: ${{ startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'renovate/') }}
3141
runs-on: ubuntu-latest
3242
timeout-minutes: 25
@@ -51,12 +61,5 @@ jobs:
5161
- name: Bridge triage (queries OSV) -> BridgeReport
5262
run: panic-attack bridge triage . --output bridge-report.json
5363

54-
- name: Apply B1 verdict -- reachable unmitigable CVE -> flag
55-
run: |
56-
blockers=$(jq '[.cves[]? | select(.classification=="Unmitigable" and .reachability.status=="Reachable")] | length' bridge-report.json)
57-
if [ "$blockers" -gt 0 ]; then
58-
echo "::error::B1: $blockers reachable unmitigable CVE(s) -> safety=flag (auto-merge blocked)"
59-
jq -r '.cves[]? | select(.classification=="Unmitigable" and .reachability.status=="Reachable") | " - \(.vulnerability.cve // .vulnerability.id): \(.rationale)"' bridge-report.json
60-
exit 1
61-
fi
62-
echo "B1 PASS: no reachable unmitigable CVEs -> arm-eligible (pool-gated)."
64+
- name: Apply B1 verdict -- validate data and reject blockers
65+
run: bash scripts/ci/check-bridge-report.sh bridge-report.json

‎.machine_readable/descriptiles/STATE.a2ml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,8 @@ new-regression-source = "test/neural_source_store_lifecycle_test.exs"
308308
authored-new-test-functions = 22
309309
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."
310310
authority = "No automatic restart, dispatch, or deployment promotion from a source refusal."
311+
312+
[bridge-report-contract-2026-11-06]
313+
status = "candidate; not merged or deployed"
314+
change = "Validate schema 0.3.0 and vulnerability-data availability before interpreting native lowercase B1 verdicts"
315+
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"

‎scripts/ci/check-bridge-report.sh‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
# Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
# Exit 0: qualified report has no B1 blockers; 1: blockers; 2: invalid/unavailable data.
5+
set -euo pipefail
6+
if [[ $# != 1 || ! -f "$1" ]]; then
7+
echo '::error::B1 assessment unavailable: expected one report file.' >&2
8+
exit 2
9+
fi
10+
# Read exactly one JSON document. jq's default stream handling could otherwise
11+
# accept an empty file or let a second document overwrite the first verdict.
12+
if ! blockers=$(jq -ers '
13+
def natural: type == "number" and . >= 0 and . == floor;
14+
if length != 1 then error("expected one report") else .[0] end
15+
| if (
16+
type == "object"
17+
and .schema_version == "0.3.0"
18+
and (.total_dependencies | natural)
19+
and (.cves | type == "array")
20+
and (
21+
(.vulnerability_data == "queried" and .total_dependencies > 0)
22+
or (.vulnerability_data == "no-dependencies" and .total_dependencies == 0 and (.cves | length) == 0)
23+
)
24+
and all(.cves[];
25+
type == "object"
26+
and (.classification | IN("mitigable", "unmitigable", "informational"))
27+
and (.reachability | type == "object")
28+
and (.reachability.status | IN("reachable", "unreachable", "phantom-declared", "phantom-transitive"))
29+
)
30+
) then [.cves[] | select(.classification == "unmitigable" and .reachability.status == "reachable")] | length
31+
else error("unsupported, incomplete or unavailable bridge assessment") end
32+
' "$1"); then
33+
echo '::error::B1 assessment unavailable: invalid report or missing vulnerability data.' >&2
34+
exit 2
35+
fi
36+
if [[ "$blockers" -gt 0 ]]; then
37+
printf '::error::B1: %s reachable unmitigable CVE(s); auto-merge blocked.\n' "$blockers" >&2
38+
exit 1
39+
fi
40+
echo 'B1 PASS: qualified report contains no reachable unmitigable CVEs.'

‎scripts/ci/test-bridge-report.sh‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
# Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
set -euo pipefail
5+
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
6+
work=$(mktemp -d)
7+
trap 'rm -rf "$work"' EXIT
8+
cases=0
9+
check() {
10+
local expected=$1 label=$2 content=$3 actual=0
11+
printf '%s' "$content" > "$work/report.json"
12+
bash "$script_dir/check-bridge-report.sh" "$work/report.json" > "$work/output" 2>&1 || actual=$?
13+
if [[ "$actual" != "$expected" ]]; then
14+
printf 'FAIL %s: expected exit %s, got %s\n' "$label" "$expected" "$actual" >&2
15+
cat "$work/output" >&2
16+
exit 1
17+
fi
18+
cases=$((cases + 1))
19+
}
20+
clean='{"schema_version":"0.3.0","vulnerability_data":"queried","total_dependencies":1,"cves":[]}'
21+
finding='{"classification":"unmitigable","reachability":{"status":"reachable"}}'
22+
blocked=$(jq --argjson finding "$finding" '.cves=[$finding]' <<< "$clean")
23+
check 0 clean "$clean"
24+
check 1 blocking "$blocked"
25+
check 0 mitigable "$(jq '.cves[0].classification="mitigable"' <<< "$blocked")"
26+
check 0 informational "$(jq '.cves[0].classification="informational" | .cves[0].reachability.status="phantom-declared"' <<< "$blocked")"
27+
check 0 no-dependencies "$(jq '.vulnerability_data="no-dependencies" | .total_dependencies=0' <<< "$clean")"
28+
check 2 offline "$(jq '.vulnerability_data="offline-unavailable"' <<< "$clean")"
29+
check 2 missing-data "$(jq 'del(.vulnerability_data)' <<< "$clean")"
30+
check 2 missing-cves "$(jq 'del(.cves)' <<< "$clean")"
31+
check 2 null-cves "$(jq '.cves=null' <<< "$clean")"
32+
check 2 object-cves "$(jq '.cves={}' <<< "$clean")"
33+
check 2 unknown-schema "$(jq '.schema_version="0.4.0"' <<< "$clean")"
34+
check 2 legacy-schema "$(jq 'del(.schema_version)' <<< "$clean")"
35+
check 2 uppercase-verdict "$(jq '.cves[0].classification="Unmitigable"' <<< "$blocked")"
36+
check 2 uppercase-reachability "$(jq '.cves[0].reachability.status="Reachable"' <<< "$blocked")"
37+
check 2 missing-classification "$(jq 'del(.cves[0].classification)' <<< "$blocked")"
38+
check 2 missing-reachability "$(jq 'del(.cves[0].reachability)' <<< "$blocked")"
39+
check 2 null-finding "$(jq '.cves=[null]' <<< "$clean")"
40+
check 2 bogus-empty "$(jq '.vulnerability_data="no-dependencies"' <<< "$blocked")"
41+
check 2 queried-zero "$(jq '.total_dependencies=0' <<< "$clean")"
42+
check 2 negative-count "$(jq '.total_dependencies=-1' <<< "$clean")"
43+
check 2 fractional-count "$(jq '.total_dependencies=1.5' <<< "$clean")"
44+
check 2 string-count "$(jq '.total_dependencies="1"' <<< "$clean")"
45+
check 2 empty ''
46+
check 2 malformed '{'
47+
check 2 null null
48+
check 2 array '[]'
49+
check 2 multiple-documents "$blocked $clean"
50+
check 2 multiple-clean-documents "$clean $clean"
51+
actual=0
52+
bash "$script_dir/check-bridge-report.sh" "$work/missing.json" > /dev/null 2>&1 || actual=$?
53+
[[ "$actual" == 2 ]]
54+
cases=$((cases + 1))
55+
printf 'PASS: %s bridge report contract cases\n' "$cases"

0 commit comments

Comments
 (0)