"""Shared finding taxonomy for IoT PreTester 4.6 Demo. The module deliberately has no Django or third-party dependencies so the same model works in the web backend, CLI, and unit tests. """ from __future__ import annotations import enum import hashlib from dataclasses import dataclass, field, asdict from typing import Any, Optional class Severity(enum.Enum): """Potential impact if the finding is valid.""" CRITICAL = "critical" HIGH = "high" MEDIUM = "medium" LOW = "low" INFO = "info" @property def rank(self) -> int: return _SEVERITY_RANK[self] class Confidence(enum.Enum): """Confidence in the evidence extracted from the capture.""" HIGH = "high" MEDIUM = "medium" LOW = "low" @property def rank(self) -> int: return _CONFIDENCE_RANK[self] class Category(enum.Enum): """Security finding categories covered by the detector set.""" PLAINTEXT_CREDENTIALS = "plaintext_credentials" PASSWORD_HASHES = "password_hashes" TOKENS_SECRETS = "tokens_secrets" MALWARE_BEACONING = "malware_beaconing" EXFILTRATION = "exfiltration" MITM = "mitm" INSECURE_PROTOCOL = "insecure_protocol" TLS_WEAKNESS = "tls_weakness" DNS_ANOMALY = "dns_anomaly" BRUTEFORCE_SCAN_FILETRANSFER = "bruteforce_scan_filetransfer" _SEVERITY_RANK = { Severity.CRITICAL: 4, Severity.HIGH: 3, Severity.MEDIUM: 2, Severity.LOW: 1, Severity.INFO: 0, } _CONFIDENCE_RANK = { Confidence.HIGH: 2, Confidence.MEDIUM: 1, Confidence.LOW: 0, } def _coerce(value, enum_cls): """Accept enum members and their serialized string values.""" if isinstance(value, enum_cls): return value if isinstance(value, str): return enum_cls(value) raise TypeError(f"{value!r} is not a valid {enum_cls.__name__}") @dataclass class Finding: """Shared data model for detector output and packet evidence.""" title: str category: Category severity: Severity confidence: Confidence risk_description: str recommendation: str affected_device: Optional[str] = None source_ip: Optional[str] = None destination_ip: Optional[str] = None protocol: Optional[str] = None port: Optional[int] = None timestamp_first_seen: Optional[float] = None timestamp_last_seen: Optional[float] = None # Structured evidence captured by the detector. evidence: dict[str, Any] = field(default_factory=dict) related_packets: list[int] = field(default_factory=list) related_streams: list[int] = field(default_factory=list) # Filled by the engine with the detector class name. detector: Optional[str] = None finding_id: Optional[str] = None def __post_init__(self) -> None: self.category = _coerce(self.category, Category) self.severity = _coerce(self.severity, Severity) self.confidence = _coerce(self.confidence, Confidence) if self.finding_id is None: self.finding_id = self.compute_id() def compute_id(self) -> str: """Build a stable identifier from the finding's identity fields.""" basis = "|".join( str(x) for x in ( self.category.value, self.title, self.source_ip, self.destination_ip, self.protocol, self.port, _evidence_fingerprint(self.evidence), ) ) return hashlib.sha1(basis.encode("utf-8")).hexdigest()[:16] @property def priority(self) -> tuple[int, int]: """Sort by severity first, then confidence.""" return (self.severity.rank, self.confidence.rank) def to_dict(self) -> dict[str, Any]: data = asdict(self) data["category"] = self.category.value data["severity"] = self.severity.value data["confidence"] = self.confidence.value return data @classmethod def from_dict(cls, data: dict[str, Any]) -> "Finding": """Reconstruct a finding from serialized data.""" fields = { "title", "category", "severity", "confidence", "risk_description", "recommendation", "affected_device", "source_ip", "destination_ip", "protocol", "port", "timestamp_first_seen", "timestamp_last_seen", "evidence", "related_packets", "related_streams", "detector", "finding_id", } kwargs = {k: v for k, v in data.items() if k in fields} return cls(**kwargs) def _evidence_fingerprint(evidence: dict[str, Any]) -> str: """Prefer an explicit deduplication key, then stable sorted evidence.""" if not evidence: return "" if "dedup_key" in evidence: return str(evidence["dedup_key"]) try: items = sorted((str(k), str(v)) for k, v in evidence.items()) except Exception: return str(evidence) return ";".join(f"{k}={v}" for k, v in items)