diff --git a/packages/secubox-cve-triage/api/main.py b/packages/secubox-cve-triage/api/main.py index 474fae4c..a5cfc37c 100644 --- a/packages/secubox-cve-triage/api/main.py +++ b/packages/secubox-cve-triage/api/main.py @@ -1090,6 +1090,64 @@ async def list_kev_cves(): return {"cves": kev_cves, "count": len(kev_cves), "source": "CISA KEV"} +# ============================================================================ +# WAF Product-Absent Probe Generator (wafgen) +# ============================================================================ +# Handlers are plain `def`, not `async def`: gather_present() shells out to +# dpkg, generate() does blocking file I/O over the vendored Nuclei subset, and +# write_category() is a blocking atomic write. This module is imported +# in-process by the secubox aggregator, which runs ONE shared event loop for +# ~110 modules — a blocking call inside an `async def` handler would freeze +# the whole board. Plain `def` handlers run in FastAPI's threadpool instead. + +@app.get("/waf-rules", dependencies=[Depends(require_jwt)]) +def waf_rules_preview(): + """Dry-run: what would be generated (kept + rejections). Writes nothing.""" + from .wafgen.generate import generate + from .wafgen.inventory import gather_present + + present, complete = gather_present() + kept, rejected = generate( + Path("/usr/lib/secubox/cve-triage/nuclei-subset"), present, complete + ) + return { + "present_count": len(present), + "inventory_complete": complete, + "kept": [ + {"cve": c.cve, "vendor": c.vendor, "product": c.product, "path": c.path} + for c in kept + ], + "rejected": [{"file": n, "reason": r} for n, r in rejected], + } + + +@app.post("/waf-rules/generate", dependencies=[Depends(require_jwt)]) +def waf_rules_generate(): + """Apply: write product_absent_probes (detect mode). Refuses if the + presence inventory is incomplete (fail-safe).""" + from .wafgen.generate import generate + from .wafgen.inventory import gather_present + from .wafgen.emit import write_category + + present, complete = gather_present() + if not complete: + raise HTTPException( + status_code=409, + detail="presence inventory incomplete — refusing (fail-safe)", + ) + kept, rejected = generate( + Path("/usr/lib/secubox/cve-triage/nuclei-subset"), present, complete + ) + now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + write_category(Path("/etc/secubox/waf/waf-rules.json"), kept, now=now) + return { + "success": True, + "written": len(kept), + "mode": "detect", + "rejected": len(rejected), + } + + # ============================================================================ # Startup # ============================================================================ diff --git a/packages/secubox-cve-triage/www/cve-triage/index.html b/packages/secubox-cve-triage/www/cve-triage/index.html index ee3694cf..2025b3db 100644 --- a/packages/secubox-cve-triage/www/cve-triage/index.html +++ b/packages/secubox-cve-triage/www/cve-triage/index.html @@ -141,6 +141,7 @@ + @@ -229,6 +230,31 @@
+ Product-absent probe candidates generated from the vendored Nuclei KEV subset. + This is a dry-run preview — nothing is written until you click Generate. + Generation refuses (409) if the presence inventory is incomplete (fail-safe). +
+| CVE | Vendor | Path |
|---|---|---|
| File | Reason |
|---|---|
${esc(c.cve)}${esc(c.path)}${esc(r.file)}