121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""Run all registered detectors and return a prioritized analysis result."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
from .context import CaptureContext
|
|
from .findings import Finding
|
|
from . import scoring
|
|
from . import provenance
|
|
from . import detectors as detectors_pkg
|
|
|
|
|
|
@dataclass
|
|
class AnalysisResult:
|
|
findings: list[Finding] = field(default_factory=list)
|
|
summary: dict = field(default_factory=dict)
|
|
meta: dict = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"meta": self.meta,
|
|
"summary": _summary_to_dict(self.summary),
|
|
"findings": [f.to_dict() for f in self.findings],
|
|
}
|
|
|
|
|
|
class AnalysisEngine:
|
|
def __init__(self, detector_list=None):
|
|
# Use every registered detector unless a test supplies a subset.
|
|
self.detectors = detector_list if detector_list is not None else detectors_pkg.build_all()
|
|
|
|
def analyze_context(self, ctx: CaptureContext) -> AnalysisResult:
|
|
started = time.time()
|
|
raw: list[Finding] = []
|
|
errors: dict[str, str] = {}
|
|
|
|
for det in self.detectors:
|
|
try:
|
|
raw.extend(list(det.detect(ctx)))
|
|
except Exception as exc: # One detector must not abort the full analysis.
|
|
errors[det.name] = repr(exc)
|
|
|
|
deduped = scoring.dedupe(raw)
|
|
ordered = scoring.sort_by_priority(deduped)
|
|
# Attach packet-level provenance before the summary is generated.
|
|
capture_file = _basename(ctx.path)
|
|
for f in ordered:
|
|
_enrich_provenance(f, capture_file)
|
|
summary = scoring.summarize(ordered)
|
|
|
|
return AnalysisResult(
|
|
findings=ordered,
|
|
summary=summary,
|
|
meta={
|
|
"capture_path": ctx.path,
|
|
"packet_count": ctx.packet_count,
|
|
"duration_seconds": round(ctx.duration, 3),
|
|
"detectors_run": [d.name for d in self.detectors],
|
|
"detector_errors": errors,
|
|
"analysis_time_seconds": round(time.time() - started, 3),
|
|
"raw_finding_count": len(raw),
|
|
"finding_count": len(ordered),
|
|
},
|
|
)
|
|
|
|
def analyze_pcap(self, path: str, packet_limit: Optional[int] = None) -> AnalysisResult:
|
|
# Keep context-only analysis usable without pyshark or tshark.
|
|
from .extract_pyshark import build_context_from_pcap
|
|
ctx = build_context_from_pcap(path, packet_limit=packet_limit)
|
|
return self.analyze_context(ctx)
|
|
|
|
|
|
def result_from_dict(data: dict) -> AnalysisResult:
|
|
"""Reconstruct a result and recalculate its summary from stored findings."""
|
|
findings = [Finding.from_dict(f) for f in data.get("findings", [])]
|
|
summary = scoring.summarize(findings)
|
|
meta = data.get("meta", {})
|
|
return AnalysisResult(findings=findings, summary=summary, meta=meta)
|
|
|
|
|
|
def _summary_to_dict(summary: dict) -> dict:
|
|
out = dict(summary)
|
|
if "highlights" in out:
|
|
out["highlights"] = [f.to_dict() for f in out["highlights"]]
|
|
if "top" in out:
|
|
out["top"] = [f.to_dict() for f in out["top"]]
|
|
return out
|
|
|
|
|
|
def _basename(path) -> Optional[str]:
|
|
if not path:
|
|
return None
|
|
import os
|
|
return os.path.basename(str(path)) or str(path)
|
|
|
|
|
|
def _enrich_provenance(f: Finding, capture_file) -> None:
|
|
"""Add capture, artifact, and packet sources without replacing detector data."""
|
|
ev = f.evidence
|
|
if capture_file and not ev.get("capture_file"):
|
|
ev["capture_file"] = capture_file
|
|
if "artifact" not in ev:
|
|
art = provenance.derive_artifact(ev)
|
|
if art is not None:
|
|
ev["artifact"] = art
|
|
if "sources" not in ev:
|
|
ev["sources"] = provenance.build_sources(
|
|
related_packets=f.related_packets,
|
|
related_streams=f.related_streams,
|
|
src_ip=f.source_ip, dst_ip=f.destination_ip,
|
|
protocol=f.protocol, port=f.port,
|
|
timestamp_first_seen=f.timestamp_first_seen,
|
|
capture=capture_file,
|
|
)
|
|
# Preserve the complete evidence count when the displayed source list is bounded.
|
|
if f.related_packets and "source_packet_count" not in ev:
|
|
ev["source_packet_count"] = len(f.related_packets)
|