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
174 changes: 174 additions & 0 deletions scripts/ci/collect_failed_check_evidence.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,178 @@ emit_strix_vulnerability_evidence() {
done <"$merged_ranges_tmp"
}

supply_chain_tool_for_label() {
# Map a failed supply-chain check label to the code-scanning SARIF tool name
# used to file its alerts, so their package/advisory/fixed-version detail can
# be pulled into the evidence as source-backed canonical lines.
local label_lower
label_lower="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
case "$label_lower" in
*osv*) printf 'OSV-Scanner' ;;
*trivy*) printf 'Trivy' ;;
*) printf '' ;;
esac
}

emit_supply_chain_alert_evidence() {
# Supply-chain scanners (osv-scanner, trivy-fs) upload SARIF to code scanning
# rather than printing findings in the failed job log. Their advisories are
# fully source-backed: each alert names the exact vulnerable package, the
# manifest file + line it is pinned on, the CVE/GHSA id, the severity, and the
# fixed version. Pull those alerts for the current head and emit canonical
# supply-chain lines so the OpenCode failed-check fallback can map them to
# concrete "bump <pkg> from <installed> to <fixed>" findings instead of a
# URL-only review. Best-effort: never fail the collector.
local label="$1"
local tool
local alerts_json
local canonical
local ref

tool="$(supply_chain_tool_for_label "$label")"
if [ -z "$tool" ]; then
return 0
fi

alerts_json="$(mktemp)"
canonical="$(mktemp)"
tmp_files+=("$alerts_json" "$canonical")

for ref in "$HEAD_SHA" "refs/pull/${PR_NUMBER}/head"; do
if gh api -X GET "repos/${GH_REPOSITORY}/code-scanning/alerts" \
-f "tool_name=${tool}" \
-f "ref=${ref}" \
-f "state=open" \
-f "per_page=100" \
--paginate >"$alerts_json" 2>/dev/null && [ -s "$alerts_json" ]; then
if jq -e 'type == "array" and length > 0' "$alerts_json" >/dev/null 2>&1; then
break
fi
fi
: >"$alerts_json"
done

if [ ! -s "$alerts_json" ]; then
return 0
fi

python3 - "$alerts_json" >"$canonical" 2>/dev/null <<'PYEOF' || true
import json
import re
import sys

try:
with open(sys.argv[1], encoding="utf-8") as handle:
alerts = json.load(handle)
except Exception:
sys.exit(0)

if not isinstance(alerts, list):
sys.exit(0)

ID_RE = re.compile(r"(CVE-\d{4}-\d{3,}|GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4})", re.I)


def first(patterns, text):
for pattern in patterns:
match = re.search(pattern, text, re.I)
if match:
return match.group(1).strip().strip("`'\"")
return ""


def clean_version(value):
return value.strip().strip("`'\"").rstrip(".,;)")


seen = set()
for alert in alerts:
if not isinstance(alert, dict):
continue
rule = alert.get("rule") or {}
instance = alert.get("most_recent_instance") or {}
location = (instance.get("location") or {})
manifest = (location.get("path") or "").strip()
line = location.get("start_line") or 0
message = ((instance.get("message") or {}).get("text") or "")
text = " ".join(
str(part)
for part in (
message,
rule.get("description") or "",
rule.get("full_description") or "",
rule.get("name") or "",
)
)
id_match = ID_RE.search(str(rule.get("id") or "")) or ID_RE.search(text)
vuln_id = id_match.group(1) if id_match else str(rule.get("id") or "").strip()
severity = (
rule.get("security_severity_level")
or rule.get("severity")
or "high"
).upper()
package = first(
[
r"Package:\s*([A-Za-z0-9._/+-]+)",
r"['\"`]([A-Za-z0-9._/+-]+)@[0-9]",
r"Package\s+['\"]([A-Za-z0-9._/+-]+?)(?:@[^'\"]*)?['\"]",
r"for (?:the )?package[:\s]+['\"`]?([A-Za-z0-9._/+-]+)['\"`]?",
],
text,
)
# A package name may still arrive as pkg@version; keep only the name.
package = package.split("@", 1)[0]
installed = clean_version(
first(
[
r"Installed Version:\s*([^\s,;]+)",
r"@([0-9][A-Za-z0-9._+-]*)",
r"currently[:\s]+([0-9][A-Za-z0-9._+-]*)",
],
text,
)
)
fixed = clean_version(
first(
[
r"Fixed Version:\s*([^\s,;]+)",
r"[Ff]ixed in[:\s]+([0-9][A-Za-z0-9._+-]*)",
r"[Pp]atched in[:\s]+([0-9][A-Za-z0-9._+-]*)",
],
text,
)
)
if not (vuln_id and package and manifest):
continue
key = (manifest.lower(), package.lower(), vuln_id.lower())
if key in seen:
continue
seen.add(key)
fields = [
f"id={vuln_id}",
f"severity={severity}",
f"package={package}",
]
if installed:
fields.append(f"installed={installed}")
if fixed:
fields.append(f"fixed={fixed}")
fields.append(f"manifest={manifest}")
if isinstance(line, int) and line > 0:
fields.append(f"line={line}")
print("- Supply-chain vulnerability: " + " ".join(fields))
PYEOF

if [ ! -s "$canonical" ]; then
return 0
fi

printf '### Supply-chain vulnerability findings\n\n'
printf 'Source-backed code-scanning alerts for this failed supply-chain check (package, manifest line, advisory id, and fixed version):\n\n'
emit_bounded_file "$canonical" 100
printf '\n'
}

owner="${GH_REPOSITORY%%/*}"
repo="${GH_REPOSITORY#*/}"
pr_node_id="$(
Expand Down Expand Up @@ -626,6 +798,8 @@ done <"$failed_contexts"
fi
fi

emit_supply_chain_alert_evidence "$label" || true

log_raw="$(mktemp)"
log_clean="$(mktemp)"
tmp_files+=("$log_raw" "$log_clean")
Expand Down
Loading
Loading