analyze performance upgrade
This commit is contained in:
@@ -105,3 +105,182 @@ def evidence_line(f: Finding) -> str:
|
||||
kv = ", ".join(f"{k}={v}" for k, v in list(extras.items())[:6])
|
||||
parts.append(kv)
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Inventory rendering shared by the Markdown and text renderers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _kb(n) -> str:
|
||||
try:
|
||||
n = int(n or 0)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
if n >= 1024 * 1024:
|
||||
return f"{n / (1024 * 1024):.1f} MB"
|
||||
if n >= 1024:
|
||||
return f"{n / 1024:.1f} kB"
|
||||
return f"{n} B"
|
||||
|
||||
|
||||
def inventory_lines(inv: dict, *, md: bool = True) -> list[str]:
|
||||
"""Render the observation inventory as a list of lines.
|
||||
|
||||
``md`` selects Markdown formatting; otherwise plain text is produced.
|
||||
"""
|
||||
if not inv:
|
||||
return []
|
||||
h2 = (lambda t: f"## {t}") if md else (lambda t: t.upper())
|
||||
h3 = (lambda t: f"### {t}") if md else (lambda t: f" {t}")
|
||||
b = (lambda t: f"- {t}") if md else (lambda t: f" - {t}")
|
||||
code = (lambda t: f"`{t}`") if md else (lambda t: str(t))
|
||||
out: list[str] = []
|
||||
|
||||
cap = inv.get("capture") or {}
|
||||
out.append(h2("Observation Inventory"))
|
||||
out.append("")
|
||||
out.append(b(f"Capture: {cap.get('packets', 0)} packets, {cap.get('duration_seconds', 0)} s, "
|
||||
f"{cap.get('internal_hosts', 0)} internal and {cap.get('external_hosts', 0)} external hosts, "
|
||||
f"{cap.get('conversations', 0)} flows"
|
||||
+ (f", VLANs {', '.join(map(str, cap['vlans']))}" if cap.get("vlans") else "")
|
||||
+ (f" · extractor {cap['extractor']}" if cap.get("extractor") else "")))
|
||||
|
||||
dev = inv.get("device") or {}
|
||||
out.append("")
|
||||
out.append(h3("Device under test"))
|
||||
if not dev.get("configured"):
|
||||
out.append(b("No device address configured."))
|
||||
for c in dev.get("candidates", [])[:5]:
|
||||
names = ", ".join(c.get("names") or []) or "—"
|
||||
out.append(b(f"Candidate {code(c['ip'])} ({names}; {', '.join(c.get('macs') or []) or 'no MAC'}; {c.get('packets')} packets)"))
|
||||
elif not dev.get("seen"):
|
||||
out.append(b(f"{code(dev.get('ip'))}: {dev.get('note')}"))
|
||||
else:
|
||||
out.append(b(f"{code(dev['ip'])} · MAC {', '.join(dev.get('macs') or []) or '—'} · "
|
||||
f"names {', '.join(dev.get('names') or []) or '—'}"
|
||||
+ (f" · DHCP vendor class {dev['dhcp_vendor_class']}" if dev.get("dhcp_vendor_class") else "")))
|
||||
out.append(b(f"{dev.get('packets')} packets · sent {_kb(dev.get('bytes_sent'))} · received {_kb(dev.get('bytes_received'))} · "
|
||||
f"first seen {fmt_ts(dev.get('first_seen'))} · last seen {fmt_ts(dev.get('last_seen'))}"))
|
||||
out.append(b(f"Services offered: {', '.join(map(str, dev.get('ports_offered') or [])) or 'none observed'} · "
|
||||
f"ports contacted: {', '.join(map(str, dev.get('ports_contacted') or [])) or '—'}"))
|
||||
out.append(b(f"Protocols: {', '.join(dev.get('protocols') or []) or '—'} · TLS sessions: {dev.get('tls_sessions', 0)} · "
|
||||
f"clear-text flows: {dev.get('cleartext_flows', 0)}"))
|
||||
if dev.get("dns_queries"):
|
||||
out.append(b("DNS names queried: " + ", ".join(dev["dns_queries"][:30])))
|
||||
peers = dev.get("peers") or []
|
||||
if peers:
|
||||
out.append(b(f"Peers ({len(peers)}):"))
|
||||
for p in peers[:40]:
|
||||
names = ", ".join(p.get("names") or [])
|
||||
out.append((" " if md else " ") + b(
|
||||
f"{code(p['ip'])}{' (' + names + ')' if names else ''} · "
|
||||
f"{'external' if p.get('external') else 'internal'} · ports {', '.join(map(str, p.get('ports') or []))} · "
|
||||
f"{', '.join(p.get('protocols') or []) or 'undecoded'} · to peer {_kb(p.get('bytes_to_peer'))} / from peer {_kb(p.get('bytes_from_peer'))}"
|
||||
).lstrip())
|
||||
|
||||
hosts = inv.get("hosts") or []
|
||||
if hosts:
|
||||
out.append("")
|
||||
out.append(h3(f"Internal hosts ({len(hosts)})"))
|
||||
for hst in hosts[:40]:
|
||||
names = ", ".join(hst.get("names") or [])
|
||||
out.append(b(f"{code(hst['ip'])}{' (' + names + ')' if names else ''}"
|
||||
f"{' ← device' if hst.get('is_device') else ''} · MAC {', '.join(hst.get('macs') or []) or '—'} · "
|
||||
f"{hst.get('packets')} packets · offers {', '.join(map(str, hst.get('ports_offered') or [])) or '—'} · "
|
||||
f"{hst.get('external_endpoints', 0)} external endpoints"))
|
||||
|
||||
eps = inv.get("external_endpoints") or []
|
||||
if eps:
|
||||
out.append("")
|
||||
out.append(h3(f"External endpoints ({len(eps)})"))
|
||||
for e in eps[:60]:
|
||||
names = ", ".join(e.get("names") or [])
|
||||
out.append(b(f"{code(e['ip'])}{' (' + names + ')' if names else ''}{' ← device' if e.get('device') else ''} · "
|
||||
f"ports {', '.join(map(str, e.get('ports') or []))} · {', '.join(e.get('protocols') or []) or 'undecoded'} · "
|
||||
f"{'encrypted' if e.get('encrypted') else 'clear-text or unknown'} · out {_kb(e.get('bytes_out'))} / in {_kb(e.get('bytes_in'))}"))
|
||||
if len(eps) > 60:
|
||||
out.append(b(f"… and {len(eps) - 60} more (see JSON report)"))
|
||||
|
||||
svcs = inv.get("services") or []
|
||||
if svcs:
|
||||
out.append("")
|
||||
out.append(h3(f"Services observed ({len(svcs)})"))
|
||||
for s in svcs[:60]:
|
||||
out.append(b(f"{code(str(s['ip']) + ':' + str(s['port']))} {s.get('service')} ({s.get('transport')})"
|
||||
f"{' ← device' if s.get('device') else ''} · {'encrypted' if s.get('encrypted') else 'clear-text' if s.get('cleartext_app') else 'unknown'} · "
|
||||
f"{s.get('flows')} flows from {len(s.get('clients') or [])} clients"))
|
||||
|
||||
protos = (inv.get("protocols") or {}).get("by_protocol") or {}
|
||||
if protos:
|
||||
out.append("")
|
||||
out.append(h3("Protocols (packets)"))
|
||||
out.append(b(", ".join(f"{k} {v}" for k, v in list(protos.items())[:30])))
|
||||
icmp = (inv.get("protocols") or {}).get("icmp") or {}
|
||||
if icmp:
|
||||
out.append(b("ICMP: " + ", ".join(f"{k} {v}" for k, v in icmp.items())))
|
||||
|
||||
tls = inv.get("tls") or {}
|
||||
if tls.get("sessions_total"):
|
||||
out.append("")
|
||||
out.append(h3(f"TLS posture ({tls['sessions_total']} handshakes)"))
|
||||
out.append(b("Versions: " + ", ".join(f"{k} ×{v}" for k, v in (tls.get("versions") or {}).items())))
|
||||
out.append(b("Cipher suites: " + ", ".join(f"{k} ×{v}" for k, v in (tls.get("ciphers") or {}).items())))
|
||||
for s in (tls.get("sessions") or [])[:30]:
|
||||
cert = s.get("certificate") or {}
|
||||
cert_txt = ""
|
||||
if cert:
|
||||
cert_txt = f" · cert {cert.get('subject')} by {cert.get('issuer')}" + (f", expires {cert.get('not_after')}" if cert.get("not_after") else "")
|
||||
out.append(b(f"{s.get('client')} → {code(str(s.get('server')) + ':' + str(s.get('port')))}"
|
||||
f"{' (' + s['sni'] + ')' if s.get('sni') else ''}{' ← device' if s.get('device') else ''} · "
|
||||
f"{s.get('version') or 'no ServerHello'} · {s.get('cipher') or '—'}"
|
||||
f"{' · client max ' + s['client_max_version'] if s.get('client_max_version') else ''}"
|
||||
f"{cert_txt}{' · mutual TLS' if s.get('mutual_tls') else ''}"
|
||||
f"{' · alerts ' + ', '.join(s['alerts']) if s.get('alerts') else ''}"))
|
||||
|
||||
dns = inv.get("dns") or {}
|
||||
if dns.get("unique_names"):
|
||||
out.append("")
|
||||
out.append(h3(f"DNS ({dns.get('queries')} queries, {dns.get('responses')} responses, {dns['unique_names']} names)"))
|
||||
out.append(b("Response codes: " + ", ".join(f"{k} {v}" for k, v in (dns.get("rcodes") or {}).items())
|
||||
+ " · resolvers: " + ", ".join(f"{k} ({v})" for k, v in (dns.get("resolvers") or {}).items())))
|
||||
for n in (dns.get("names") or [])[:60]:
|
||||
out.append(b(f"{code(n['name'])}{' ← device' if n.get('device') else ''} · {n.get('queries')}× · "
|
||||
f"{', '.join(n.get('types') or [])} · {', '.join(n.get('answers') or []) or 'no answer seen'}"))
|
||||
|
||||
http = inv.get("http") or {}
|
||||
if http.get("requests"):
|
||||
out.append("")
|
||||
out.append(h3(f"HTTP ({http['requests']} requests)"))
|
||||
for hh in (http.get("hosts") or [])[:30]:
|
||||
out.append(b(f"{code(hh['host'])}{' ← device' if hh.get('device') else ''} · {hh.get('requests')} requests · "
|
||||
f"{', '.join(hh.get('methods') or [])} · {', '.join(hh.get('uris') or [])[:200]}"
|
||||
f"{' · with Authorization header' if hh.get('auth') else ''}"))
|
||||
for ft in (http.get("file_transfers") or [])[:20]:
|
||||
out.append(b(f"File transfer: {ft.get('filename') or ft.get('uri') or ft.get('content_type')} "
|
||||
f"({ft.get('protocol')}, {ft.get('direction')}, {ft.get('src_ip')} → {ft.get('dst_ip')})"))
|
||||
|
||||
ct = inv.get("cleartext") or {}
|
||||
if ct.get("flows") or ct.get("service_events") or ct.get("dhcp"):
|
||||
out.append("")
|
||||
out.append(h3("Clear-text protocols and service events"))
|
||||
for f in ct.get("flows") or []:
|
||||
out.append(b(f"{f['protocol']}{' ← device' if f.get('device') else ''}: {f.get('flows')} flows, {_kb(f.get('bytes'))} · "
|
||||
+ "; ".join((f.get("pairs") or [])[:6])))
|
||||
for s in (ct.get("service_events") or [])[:30]:
|
||||
out.append(b(f"{s.get('protocol')} {s.get('kind')}: {s.get('value') or ''} {s.get('detail') or ''} "
|
||||
f"({s.get('src_ip')} → {s.get('dst_ip')})".strip()))
|
||||
for d in (ct.get("dhcp") or [])[:20]:
|
||||
out.append(b(f"DHCP {d.get('type')}: {d.get('client_mac')} {d.get('hostname') or ''} "
|
||||
f"{d.get('vendor_class') or ''} → {d.get('client_ip') or ''} (server {d.get('server') or '—'})".strip()))
|
||||
|
||||
cov = inv.get("coverage") or {}
|
||||
out.append("")
|
||||
out.append(h3("Coverage"))
|
||||
out.append(b(f"{'Complete' if cov.get('complete') else 'PARTIAL'} extraction · {cov.get('flows_total', 0)} flows, "
|
||||
f"{cov.get('flows_encrypted', 0)} encrypted, {cov.get('flows_undecoded', 0)} with undecoded payload · "
|
||||
f"{cov.get('text_payloads_scanned', 0)} clear-text payloads scanned"))
|
||||
if cov.get("undecoded_ports"):
|
||||
out.append(b("Undecoded ports (flows): " + ", ".join(f"{k}: {v}" for k, v in cov["undecoded_ports"].items())))
|
||||
for n in cov.get("notes") or []:
|
||||
out.append(b(n))
|
||||
return out
|
||||
|
||||
@@ -6,6 +6,7 @@ from ..engine import AnalysisResult
|
||||
from ..findings import Severity
|
||||
from ._common import (
|
||||
SEVERITY_LABEL, fmt_ts, affected_line, evidence_line, artifact_line, sources_list,
|
||||
inventory_lines,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +21,12 @@ def render(result: AnalysisResult, extra: dict | None = None) -> str:
|
||||
out.append(f"- **Packets:** {m.get('packet_count', 0)} · "
|
||||
f"**Duration:** {m.get('duration_seconds', 0)} s · "
|
||||
f"**Findings:** {m.get('finding_count', 0)}")
|
||||
if m.get("device_ip"):
|
||||
out.append(f"- **Device under test:** `{m['device_ip']}`"
|
||||
+ (f" (MAC {', '.join(m.get('device_macs') or [])})" if m.get("device_macs") else "")
|
||||
+ f" · device-related findings: {s.get('device_related', 0)}, other hosts: {s.get('other_hosts', 0)}")
|
||||
if m.get("truncated"):
|
||||
out.append(f"- **⚠️ Partial analysis:** {m.get('truncation_reason')}")
|
||||
out.append("")
|
||||
|
||||
bysev = s.get("by_severity", {})
|
||||
@@ -62,12 +69,18 @@ def render(result: AnalysisResult, extra: dict | None = None) -> str:
|
||||
for source in srcs:
|
||||
out.append(f" - {source}")
|
||||
out.append(f"- **Risk:** {f.risk_description}")
|
||||
out.append(f"- **Affected:** {affected_line(f)}")
|
||||
out.append(f"- **Affected:** {affected_line(f)}"
|
||||
+ (" · **involves device under test**" if f.evidence.get("device_related") else ""))
|
||||
out.append(f"- **Additional evidence:** {evidence_line(f)}")
|
||||
out.append(f"- **Recommendation:** {f.recommendation}")
|
||||
out.append(f"- **Finding ID:** `{f.finding_id}`")
|
||||
out.append("")
|
||||
|
||||
inv_lines = inventory_lines(result.inventory, md=True)
|
||||
if inv_lines:
|
||||
out.append("")
|
||||
out.extend(inv_lines)
|
||||
|
||||
# Optional URL and hash enrichment results.
|
||||
if extra:
|
||||
hashes = extra.get("hashes") or []
|
||||
|
||||
@@ -6,6 +6,7 @@ from ..engine import AnalysisResult
|
||||
from ..findings import Severity
|
||||
from ._common import (
|
||||
SEVERITY_LABEL, affected_line, evidence_line, artifact_line, sources_list,
|
||||
inventory_lines,
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +22,11 @@ def render(result: AnalysisResult, extra: dict | None = None) -> str:
|
||||
out.append(f"Capture : {m.get('capture_path', '-')}")
|
||||
out.append(f"Packets : {m.get('packet_count', 0)} Duration: {m.get('duration_seconds', 0)}s")
|
||||
out.append(f"Findings: {m.get('finding_count', 0)}")
|
||||
if m.get("device_ip"):
|
||||
out.append(f"Device : {m['device_ip']} (device-related findings: {s.get('device_related', 0)}, "
|
||||
f"other hosts: {s.get('other_hosts', 0)})")
|
||||
if m.get("truncated"):
|
||||
out.append(f"WARNING : partial analysis - {m.get('truncation_reason')}")
|
||||
out.append("")
|
||||
|
||||
bysev = s.get("by_severity", {})
|
||||
@@ -62,6 +68,12 @@ def render(result: AnalysisResult, extra: dict | None = None) -> str:
|
||||
out.append(f" Finding ID : {f.finding_id}")
|
||||
out.append("")
|
||||
|
||||
inv_lines = inventory_lines(result.inventory, md=False)
|
||||
if inv_lines:
|
||||
out.append("-" * 70)
|
||||
out.extend(inv_lines)
|
||||
out.append("")
|
||||
|
||||
if extra:
|
||||
hashes = extra.get("hashes") or []
|
||||
urls = extra.get("urls") or []
|
||||
|
||||
Reference in New Issue
Block a user