feat: tool normalizers, Exploit-DB grounding, orchestrator, report, triage, CLI

This commit is contained in:
2026-06-29 17:36:04 -04:00
parent 1f531e4948
commit aa4501f2fa
18 changed files with 1511 additions and 0 deletions
@@ -0,0 +1,90 @@
"""httpx — HTTP probing: status, title, tech, webserver, TLS. Output: ``-json -td``."""
from __future__ import annotations
from dataclasses import dataclass
from ..schema import HttpInfo, TlsInfo
from .base import parse_jsonl, run_tool
BINARY = "httpx"
@dataclass
class HttpProbe:
host: str | None
ip: str | None
port: int | None
info: HttpInfo
def _port_of(rec: dict) -> int | None:
port = rec.get("port")
try:
return int(port)
except (TypeError, ValueError):
return None
def _tls_of(rec: dict) -> TlsInfo | None:
tls = rec.get("tls")
if not isinstance(tls, dict):
return None
issuer = tls.get("issuer_cn") or tls.get("issuer_common_name")
if not issuer:
org = tls.get("issuer_org")
if isinstance(org, list) and org:
issuer = org[0]
elif isinstance(org, str):
issuer = org
return TlsInfo(
subject_cn=tls.get("subject_cn") or tls.get("subject_common_name"),
issuer=issuer,
not_after=tls.get("not_after"),
)
def normalize(raw: str) -> list[HttpProbe]:
"""Map httpx -json lines into HttpProbe records keyed by host/ip/port."""
out: list[HttpProbe] = []
for rec in parse_jsonl(raw):
tech = rec.get("tech") or rec.get("technologies") or []
if isinstance(tech, str):
tech = [tech]
info = HttpInfo(
url=rec.get("url"),
status=rec.get("status_code") or rec.get("status-code"),
title=rec.get("title"),
webserver=rec.get("webserver"),
technologies=[str(t) for t in tech],
tls=_tls_of(rec),
)
out.append(
HttpProbe(
host=(rec.get("input") or rec.get("host") or "").lower() or None,
ip=rec.get("host") if _looks_like_ip(rec.get("host")) else rec.get("a", [None])[0] if isinstance(rec.get("a"), list) else None,
port=_port_of(rec),
info=info,
)
)
return out
def _looks_like_ip(v) -> bool:
import ipaddress
if not isinstance(v, str):
return False
try:
ipaddress.ip_address(v)
return True
except ValueError:
return False
def run(targets: list[str], *, timeout: float = 180.0, rate_limit: int | None = None) -> tuple[list[HttpProbe], object]:
cmd = [BINARY, "-json", "-td", "-silent", "-title", "-tech-detect", "-web-server", "-status-code"]
if rate_limit:
cmd += ["-rate-limit", str(rate_limit)]
res = run_tool(cmd, timeout=timeout, input_text="\n".join(targets) + "\n")
return normalize(res.stdout), res