Files
IoT_PreTester_V46_Demo/django_project/analyzer/engine.py
T
2026-08-19 08:04:59 +02:00

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)