Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions .github/workflows/bridge-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
# Notes for review:
# - B2 (major -> review) is left to the existing human-review path; it can be added
# later via dependabot/fetch-metadata.
# - panic-attack is built from source (public clone). Production hardening: a released
# binary, or -- to match estate convention -- a hyperpolymath/standards reusable
# workflow. Offered as a follow-up.
# - panic-attack is built at the commit that static-analysis-gate.yml pins in
# PANIC_ATTACK_REF. That file owns the pin: this job reads it, so the two
# workflows cannot use different scanners and no moving ref is built (#481).
# - github context is passed via env: only (no ${{ }} in run lines) per estate rules.
name: bridge-gate
on:
Expand All @@ -44,21 +44,45 @@ jobs:
exit 1
fi

- name: Build panic-attack (Patch-Bridge)
- name: Build panic-attack at the Static Analysis Gate pin (Patch-Bridge)
run: |
git clone --depth 1 https://github.com/hyperpolymath/panic-attack /tmp/pa
cargo build --release --features http --manifest-path /tmp/pa/Cargo.toml
echo "/tmp/pa/target/release" >> "$GITHUB_PATH"
set -euo pipefail
# One pin for both workflows. A missing or floating value builds nothing.
ref=$(yq -r '.env.PANIC_ATTACK_REF' .github/workflows/static-analysis-gate.yml)
if ! [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::static-analysis-gate.yml has no full-SHA PANIC_ATTACK_REF (got '${ref}'); no scanner was built."
exit 2
fi
echo "PANIC_ATTACK_REF=$ref" >> "$GITHUB_ENV"
tool_root="$RUNNER_TEMP/panic-toolchain"
# Same build as the Static Analysis Gate: exact revision, locked
# dependencies, and proof that the installed binary names that revision.
timeout 600 cargo install --git https://github.com/hyperpolymath/panic-attack \
--rev "$ref" --locked --features http --root "$tool_root" panic-attack
grep -Fq "#$ref" "$tool_root/.crates.toml"
"$tool_root/bin/panic-attack" --version
jq -cn --arg sha "$ref" \
--arg digest "$(sha256sum "$tool_root/bin/panic-attack" | cut -d' ' -f1)" \
--arg rust "$(rustc --version)" --arg cargo "$(cargo --version)" \
'{source_sha:$sha,binary_sha256:$digest,features:["http"],rustc:$rust,cargo:$cargo}' \
> "$tool_root/source.json"
echo "$tool_root/bin" >> "$GITHUB_PATH"

- name: Bridge triage (queries OSV) -> BridgeReport
run: panic-attack bridge triage . --output bridge-report.json
# The adapter that the Static Analysis Gate uses. It checks the binary
# against its build receipt and writes a receipt only for a complete
# schema 0.3.0 report whose vulnerability_data is "queried".
- name: Bridge triage (queries OSV) -> qualified BridgeReport
env:
SCAN_EXPECTED_SHA: ${{ github.sha }}
run: |
bash scripts/ci/static-analysis-evidence.sh scan patch-bridge "$PANIC_ATTACK_REF" \
"$RUNNER_TEMP/panic-toolchain/bin/panic-attack" "$RUNNER_TEMP/panic-toolchain/source.json" \
"$RUNNER_TEMP/bridge-report"

# Exit 1: a reachable unmitigable CVE. Exit 2: no qualified report, so
# there is no CVE result to judge. Controls for both are in
# scripts/ci/test-static-analysis-evidence.sh.
- 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)."
env:
SCAN_EXPECTED_SHA: ${{ github.sha }}
run: bash scripts/ci/static-analysis-evidence.sh bridge-b1 "$RUNNER_TEMP/bridge-report/receipt.json"
36 changes: 35 additions & 1 deletion scripts/ci/static-analysis-evidence.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,43 @@ deposit() {
+ [$hyp[0][] | . + {scanner:"hypatia"}]),patch_bridge:$bridge[0]}' > "$dir/unified-findings.json"
}

# Merge-orchestration B1 (bridge-gate.yml): a reachable unmitigable CVE blocks a
# dependency bump. Exit 1 is that verdict. Exit 2 means there is no CVE result
# to judge: the receipt is not a qualified Patch Bridge receipt (which requires
# vulnerability_data == "queried"), or a CVE carries a reachability status this
# gate does not know. An unknown value must never count as "not reachable".
bridge_b1() {
local receipt=$1 report blockers
local reachable_unmitigable='.cves[] | select(.classification == "unmitigable" and .reachability.status == "reachable")'
one_json "$receipt" || return 2
report=$(mktemp)
if [[ "$(jq -r '.tool' "$receipt")" != patch-bridge ]] ||
! qualified_receipt patch-bridge "${PANIC_ATTACK_REF:?}" "$receipt" "$report"; then
rm -f -- "$report"
echo '::error::B1: no qualified Patch Bridge report. The vulnerability feed must be queried; this is not a CVE result.' >&2
return 2
fi
if ! jq -e 'all(.cves[]; .reachability.status as $s |
["phantom-declared","phantom-transitive","unreachable","reachable"] | index($s) != null)' "$report" >/dev/null 2>&1; then
rm -f -- "$report"
echo '::error::B1: a CVE has no known reachability status; the report does not match the pinned contract.' >&2
return 2
fi
blockers=$(jq "[$reachable_unmitigable] | length" "$report")
if [[ "$blockers" -gt 0 ]]; then
echo "::error::B1: $blockers reachable unmitigable CVE(s) -> safety=flag (auto-merge blocked)"
jq -r "$reachable_unmitigable"' | " - \(.vulnerability.cve // .vulnerability.id // "unidentified"): \(.rationale // "no rationale")"' "$report"
rm -f -- "$report"
return 1
fi
rm -f -- "$report"
echo 'B1 PASS: the feed was queried and no unmitigable CVE is reachable -> arm-eligible (pool-gated).'
}

case "${1:-}" in
scan) shift; scan "$@" ;;
gate) shift; gate "$@" ;;
deposit) shift; deposit "$@" ;;
*) echo 'usage: static-analysis-evidence.sh scan TOOL REF BIN SOURCE-RECEIPT OUTPUT-DIR | gate RECEIPT | deposit ARTIFACT-DIR' >&2; exit 2 ;;
bridge-b1) shift; bridge_b1 "$@" ;;
*) echo 'usage: static-analysis-evidence.sh scan TOOL REF BIN SOURCE-RECEIPT OUTPUT-DIR | gate RECEIPT | deposit ARTIFACT-DIR | bridge-b1 RECEIPT' >&2; exit 2 ;;
esac
52 changes: 52 additions & 0 deletions scripts/ci/test-static-analysis-evidence.sh
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,58 @@ expect_exit 2 bridge
printf '%s\n' '{"schema_version":"0.2.0","total_dependencies":2,"vulnerable_dependencies":1,"cves":[{"classification":"unmitigable"}],"mitigated":0,"unmitigable":1,"concatenative":0,"informational":0}' > "$MOCK_REPORT"
expect_exit 2 bridge

# B1 (bridge-gate.yml): exit 1 only for a reachable unmitigable CVE in a
# qualified report. No qualified report, or an unknown reachability value, is
# exit 2: it is never a pass.
b1() { bash "$adapter" bridge-b1 "$tmp/bridge/receipt.json"; }
bridge_report() { # <vulnerability_data> <classification> <reachability status>
local data=$1 class=$2 reach=$3
jq -cn --arg data "$data" --arg class "$class" --arg reach "$reach" '
{schema_version:"0.3.0",vulnerability_data:$data,total_dependencies:2,vulnerable_dependencies:1,
cves:[{classification:$class,reachability:{status:$reach},vulnerability:{id:"FIXTURE-0001"},rationale:"planted"}],
mitigated:(if $class == "mitigable" then 1 else 0 end),unmitigable:(if $class == "unmitigable" then 1 else 0 end),
concatenative:0,informational:(if $class == "informational" then 1 else 0 end)}' > "$MOCK_REPORT"
}
bridge_report queried unmitigable reachable
expect_exit 0 bridge
expect_exit 1 b1
grep -Fq 'FIXTURE-0001: planted' "$tmp/control.log"
# Paired: the same CVE does not block when it is not reachable or is mitigable.
for reach in unreachable phantom-declared phantom-transitive; do
bridge_report queried unmitigable "$reach"
expect_exit 0 bridge
expect_exit 0 b1
done
bridge_report queried mitigable reachable
expect_exit 0 bridge
expect_exit 0 b1
printf '%s\n' '{"schema_version":"0.3.0","vulnerability_data":"queried","total_dependencies":2,"vulnerable_dependencies":0,"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > "$MOCK_REPORT"
expect_exit 0 bridge
expect_exit 0 b1
# The spelling the workflow compared before #481 never matched the report.
# An unknown status is refused, so a renamed value cannot pass in silence.
bridge_report queried unmitigable Reachable
expect_exit 0 bridge
expect_exit 2 b1
# A report whose feed was not queried leaves no qualified receipt to judge.
for data in offline-unavailable no-dependencies; do
bridge_report "$data" unmitigable reachable
expect_exit 2 bridge
expect_exit 2 b1
done
# The verdict checks the report itself, not only that a receipt exists: a
# receipt whose report says the feed was not queried, or says nothing about
# the feed, is refused. Without that check this zero-CVE report would pass.
printf '%s\n' '{"schema_version":"0.3.0","vulnerability_data":"queried","total_dependencies":2,"vulnerable_dependencies":0,"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > "$MOCK_REPORT"
expect_exit 0 bridge
jq '.report.vulnerability_data = "offline-unavailable"' "$tmp/bridge/receipt.json" > "$tmp/unqueried-receipt.json"
expect_exit 2 bash "$adapter" bridge-b1 "$tmp/unqueried-receipt.json"
jq 'del(.report.vulnerability_data)' "$tmp/bridge/receipt.json" > "$tmp/unqueried-receipt.json"
expect_exit 2 bash "$adapter" bridge-b1 "$tmp/unqueried-receipt.json"
# Another scanner's receipt and a missing receipt are not CVE results.
expect_exit 2 bash "$adapter" bridge-b1 "$tmp/qualified-hypatia.json"
expect_exit 2 bash "$adapter" bridge-b1 "$tmp/no-such-receipt.json"

# Deposit preserves actual critical findings even though their gate failed.
# Only mutually matching completed scan receipts may create the legacy artifact.
mkdir -p "$tmp/findings/"{panic-attack-findings,hypatia-findings,bridge-report}
Expand Down
Loading