SecBrief / backend /alert_parser.py
ADI576's picture
SecBrief
7b92798
Raw History Blame Contribute Delete
3.98 kB
"""Parse SARIF, npm audit JSON, and other scanner formats into normalized alert text."""
from __future__ import annotations
import json
import re
from typing import Any
def detect_format(text: str, filename: str = "") -> str:
name = filename.lower()
stripped = text.strip()
if name.endswith(".sarif") or name.endswith(".json"):
if '"runs"' in stripped and '"results"' in stripped:
return "sarif"
if '"vulnerabilities"' in stripped or '"metadata"' in stripped and '"vulnerabilities"' in stripped:
return "npm_audit"
if stripped.startswith("{"):
try:
data = json.loads(stripped)
if isinstance(data, dict):
if "runs" in data:
return "sarif"
if "vulnerabilities" in data or data.get("auditReportVersion"):
return "npm_audit"
except json.JSONDecodeError:
pass
return "text"
def parse_upload(text: str, filename: str = "") -> dict[str, Any]:
fmt = detect_format(text, filename)
if fmt == "sarif":
return _parse_sarif(text, filename)
if fmt == "npm_audit":
return _parse_npm_audit(text, filename)
return {
"format": "text",
"filename": filename,
"finding_count": 1,
"alert_text": text.strip(),
"title": "Uploaded security report",
}
def _parse_sarif(raw: str, filename: str) -> dict[str, Any]:
data = json.loads(raw)
lines: list[str] = ["SARIF security report", "=" * 40, ""]
count = 0
for run in data.get("runs", []):
tool = run.get("tool", {}).get("driver", {})
tool_name = tool.get("name", "scanner")
lines.append(f"Tool: {tool_name}")
for res in run.get("results", [])[:40]:
count += 1
rule = res.get("ruleId", "unknown")
level = res.get("level", "warning")
msg = res.get("message", {})
text = msg.get("text") if isinstance(msg, dict) else str(msg)
locs = res.get("locations", [])
path = ""
if locs:
phys = locs[0].get("physicalLocation", {}).get("artifactLocation", {})
path = phys.get("uri", "")
lines.append(f"\n[{count}] {level.upper()} — {rule}")
if path:
lines.append(f" Location: {path}")
lines.append(f" {text}")
lines.append("")
alert = "\n".join(lines)
return {
"format": "sarif",
"filename": filename,
"finding_count": count,
"alert_text": alert,
"title": f"SARIF report ({count} finding(s))",
}
def _parse_npm_audit(raw: str, filename: str) -> dict[str, Any]:
data = json.loads(raw)
vulns = data.get("vulnerabilities") or {}
lines = ["npm audit report", "=" * 40, ""]
count = 0
for name, v in list(vulns.items())[:50]:
count += 1
sev = v.get("severity", "unknown")
via = v.get("via", [])
title = name
if via and isinstance(via[0], dict):
title = via[0].get("title", name)
fix = v.get("fixAvailable")
fix_str = "fix available" if fix else "no automatic fix"
lines.append(f"{name} — {sev} — {title} ({fix_str})")
if count == 0 and data.get("metadata", {}).get("vulnerabilities"):
meta = data["metadata"]["vulnerabilities"]
lines.append(
f"Summary: {meta.get('high', 0)} high, {meta.get('moderate', 0)} moderate, "
f"{meta.get('low', 0)} low"
)
count = sum(meta.values()) if isinstance(meta, dict) else 1
alert = "\n".join(lines)
return {
"format": "npm_audit",
"filename": filename,
"finding_count": max(count, 1),
"alert_text": alert,
"title": f"npm audit ({count} package(s))",
}
def extract_cve_ids(text: str) -> list[str]:
return list(dict.fromkeys(re.findall(r"CVE-\d{4}-\d+", text, re.I)))