README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-5281 Vulnerability Scanner
A simple, consolidated Python script to audit Chrome versions locally and via inventory files,
as well as triage crash logs, to detect potential vulnerability to CVE-2026-5281 (WebGPU Use-After-Free).
Patched Version: 146.0.7680.178
"""
import argparse
import csv
import json
import os
import platform
import re
import subprocess
import sys
import winreg
from pathlib import Path
PATCHED_VERSION = "146.0.7680.178"
FATAL_LOG_PATTERNS = [
re.compile(r"gpu device lost", re.IGNORECASE),
re.compile(r"crash detected", re.IGNORECASE),
re.compile(r"vulnerability confirmed", re.IGNORECASE),
re.compile(r"uncaught gpu error.*(device lost|gpu hang|context lost|out of memory|internal error|removed)", re.IGNORECASE),
]
NON_FATAL_LOG_PATTERNS = [
re.compile(r"max attempts reached without crash", re.IGNORECASE),
re.compile(r"no exploit signatures detected", re.IGNORECASE),
]
# --- Version Utilities ---
def parse_chrome_version(version: str):
if not version:
return None
parts = version.strip().split(".")
if len(parts) != 4:
return None
try:
parsed = tuple(int(p) for p in parts)
except ValueError:
return None
if any(p < 0 for p in parsed):
return None
return parsed
def compare_versions(left: str, right: str):
l = parse_chrome_version(left)
r = parse_chrome_version(right)
if l is None or r is None:
return None
if l < r:
return -1
if l > r:
return 1
return 0
def is_vulnerable(version: str):
cmp_result = compare_versions(version, PATCHED_VERSION)
if cmp_result is None:
return None, "Invalid version format"
if cmp_result < 0:
return True, f"Version is below patched release {PATCHED_VERSION}"
return False, f"Version is at or above patched release {PATCHED_VERSION}"
# --- Local Audit ---
def get_registry_versions():
records = []
if platform.system() != "Windows":
return records
paths = [
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Google\Chrome\BLBeacon"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Google\Chrome\BLBeacon")
]
for hkey, subkey in paths:
try:
with winreg.OpenKey(hkey, subkey) as key:
version, _ = winreg.QueryValueEx(key, "version")
vuln, reason = is_vulnerable(version)
records.append({
"source": "registry",
"path": f"HKEY_...\\{subkey}",
"version": version,
"vulnerable": vuln,
"reason": reason
})
except OSError:
pass
return records
def get_binary_versions():
records = []
if platform.system() != "Windows":
return records
common_paths = [
Path(os.environ.get("ProgramFiles", "C:\\Program Files")) / "Google" / "Chrome" / "Application" / "chrome.exe",
Path(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")) / "Google" / "Chrome" / "Application" / "chrome.exe",
Path(os.environ.get("LOCALAPPDATA", "")) / "Google" / "Chrome" / "Application" / "chrome.exe",
]
for p in common_paths:
if p.exists():
try:
cmd = f'(Get-Item "{p}").VersionInfo.ProductVersion'
output = subprocess.check_output(["powershell", "-c", cmd], text=True).strip()
version = output.split()[0] if output else None
if version:
vuln, reason = is_vulnerable(version)
records.append({
"source": "binary",
"path": str(p),
"version": version,
"vulnerable": vuln,
"reason": reason
})
except Exception:
pass
return records
def run_local_audit(json_output=False):
results = get_registry_versions() + get_binary_versions()
if json_output:
print(json.dumps(results, indent=2))
return
print(f"\n[+] Local System Audit for CVE-2026-5281")
if not results:
print(" [-] No Chrome installations found.")
return
for r in results:
status = "VULNERABLE" if r["vulnerable"] else "SAFE" if r["vulnerable"] is False else "UNKNOWN"
print(f" [{status}] {r['path']} -> v{r['version']}")
# --- Fleet Audit ---
def run_fleet_audit(csv_path: str, json_output=False):
results = []
try:
with open(csv_path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
version = row.get("version", "").strip()
vuln, reason = is_vulnerable(version)
results.append({
"host": row.get("host", "unknown"),
"product": row.get("product", "unknown"),
"version": version,
"vulnerable": vuln,
"reason": reason
})
except Exception as e:
print(f"Error reading {csv_path}: {e}")
return
if json_output:
print(json.dumps(results, indent=2))
return
print(f"\n[+] Fleet Audit for CVE-2026-5281 ({len(results)} hosts)")
for r in results:
status = "VULNERABLE" if r["vulnerable"] else "SAFE" if r["vulnerable"] is False else "UNKNOWN"
print(f" [{status}] Host: {r['host']} | Product: {r['product']} | Version: {r['version']}")
# --- Log Triage ---
def run_log_triage(log_path: str):
p = Path(log_path)
if not p.exists():
print(f"Log path does not exist: {log_path}")
return
signatures = [
re.compile(r"use-after-free", re.IGNORECASE),
re.compile(r"webgpu", re.IGNORECASE),
re.compile(r"commandbuffer", re.IGNORECASE),
re.compile(r"dawn::", re.IGNORECASE)
]
print(f"\n[+] Triaging logs in {p} for CVE-2026-5281 signatures...")
files_to_check = [p] if p.is_file() else p.rglob("*.log")
found_any = False
for fpath in files_to_check:
try:
with open(fpath, "r", encoding="utf-8", errors="ignore") as f:
for num, line in enumerate(f, 1):
# Check if line indicates a UAF in WebGPU
if "use-after-free" in line.lower() and ("webgpu" in line.lower() or "dawn" in line.lower() or "commandbuffer" in line.lower()):
print(f" [!] SUSPICIOUS LINE in {fpath.name} (Line {num}):")
print(f" {line.strip()}")
found_any = True
except Exception:
pass
if not found_any:
print(" [-] No exploit signatures detected in provided logs.")
# --- Claim Readiness Assessment ---
def load_text(path: str):
p = Path(path)
if not p.exists() or not p.is_file():
raise FileNotFoundError(f"File not found: {path}")
return p.read_text(encoding="utf-8", errors="ignore")
def analyze_log_markers(content: str):
fatal_hits = []
safe_hits = []
for pattern in FATAL_LOG_PATTERNS:
if pattern.search(content):
fatal_hits.append(pattern.pattern)
for pattern in NON_FATAL_LOG_PATTERNS:
if pattern.search(content):
safe_hits.append(pattern.pattern)
return {
"fatal_hits": fatal_hits,
"safe_hits": safe_hits,
"has_fatal": len(fatal_hits) > 0,
"has_safe": len(safe_hits) > 0,
}
def run_claim_assessment(vuln_log: str, patched_log: str, vuln_version: str = None, patched_version: str = None, json_output: bool = False):
vuln_content = load_text(vuln_log)
patched_content = load_text(patched_log)
vuln_markers = analyze_log_markers(vuln_content)
patched_markers = analyze_log_markers(patched_content)
checks = []
checks.append({
"name": "vulnerable_run_shows_fatal_gpu_behavior",
"pass": vuln_markers["has_fatal"],
"details": vuln_markers["fatal_hits"],
})
checks.append({
"name": "patched_run_does_not_show_fatal_gpu_behavior",
"pass": not patched_markers["has_fatal"],
"details": patched_markers["fatal_hits"],
})
if vuln_version:
is_vuln, reason = is_vulnerable(vuln_version)
checks.append({
"name": "vulnerable_version_is_below_fixed_threshold",
"pass": is_vuln is True,
"details": reason,
})
if patched_version:
is_vuln, reason = is_vulnerable(patched_version)
checks.append({
"name": "patched_version_is_at_or_above_fixed_threshold",
"pass": is_vuln is False,
"details": reason,
})
passed = sum(1 for c in checks if c["pass"])
total = len(checks)
verdict = "READY"
if passed < total:
verdict = "PARTIAL"
if passed <= max(1, total // 2):
verdict = "INSUFFICIENT"
output = {
"patched_version_threshold": PATCHED_VERSION,
"verdict": verdict,
"score": {
"passed": passed,
"total": total,
},
"checks": checks,
"evidence": {
"vulnerable_log": vuln_log,
"patched_log": patched_log,
"vulnerable_log_markers": vuln_markers,
"patched_log_markers": patched_markers,
},
}
if json_output:
print(json.dumps(output, indent=2))
return
print("\n[+] Claim Readiness Assessment for CVE-2026-5281")
print(f" Verdict: {verdict}")
print(f" Score: {passed}/{total}")
for c in checks:
state = "PASS" if c["pass"] else "FAIL"
print(f" [{state}] {c['name']}")
if c["details"]:
print(f" {c['details']}")
# --- Main ---
def main():
parser = argparse.ArgumentParser(description="CVE-2026-5281 Universal Scanner & Audit Tool")
parser.add_argument("--local", action="store_true", help="Run a local Chrome installation audit")
parser.add_argument("--fleet", metavar="CSV_FILE", help="Run a fleet audit against a CSV inventory file (needs host, product, version columns)")
parser.add_argument("--triage", metavar="LOG_DIR_OR_FILE", help="Triage crash logs for WebGPU/Dawn Use-After-Free signatures")
parser.add_argument("--assess-claim", action="store_true", help="Assess claim readiness using vulnerable and patched run logs")
parser.add_argument("--vuln-log", metavar="LOG_FILE", help="Path to vulnerable test run log")
parser.add_argument("--patched-log", metavar="LOG_FILE", help="Path to patched test run log")
parser.add_argument("--vuln-version", metavar="VERSION", help="Browser version used for vulnerable run")
parser.add_argument("--patched-version", metavar="VERSION", help="Browser version used for patched run")
parser.add_argument("--json", action="store_true", help="Output results in JSON format (applies to --local and --fleet)")
args = parser.parse_args()
if not any([args.local, args.fleet, args.triage, args.assess_claim]):
parser.print_help()
return
if args.local:
run_local_audit(args.json)
if args.fleet:
run_fleet_audit(args.fleet, args.json)
if args.triage:
run_log_triage(args.triage)
if args.assess_claim:
if not args.vuln_log or not args.patched_log:
print("Error: --assess-claim requires --vuln-log and --patched-log")
sys.exit(2)
run_claim_assessment(
vuln_log=args.vuln_log,
patched_log=args.patched_log,
vuln_version=args.vuln_version,
patched_version=args.patched_version,
json_output=args.json,
)
if __name__ == "__main__":
main()