"""Threat actor domain entity with coverage analysis logic. Pure domain logic — no framework imports. """ from __future__ import annotations import uuid from dataclasses import dataclass, field from typing import Any @dataclass class ThreatActorTechniqueRef: """Lightweight reference to a technique used by an actor.""" technique_id: uuid.UUID mitre_id: str | None = None name: str | None = None status: str | None = None usage_description: str | None = None @dataclass class ThreatActorEntity: name: str id: uuid.UUID | None = None mitre_id: str | None = None aliases: list[str] = field(default_factory=list) description: str | None = None country: str | None = None target_sectors: list[str] = field(default_factory=list) target_regions: list[str] = field(default_factory=list) motivation: str | None = None sophistication: str | None = None first_seen: str | None = None last_seen: str | None = None is_active: bool = True techniques: list[ThreatActorTechniqueRef] = field(default_factory=list) @property def technique_count(self) -> int: return len(self.techniques) @property def covered_techniques(self) -> list[ThreatActorTechniqueRef]: return [ t for t in self.techniques if t.status in ("validated", "partial") ] @property def uncovered_techniques(self) -> list[ThreatActorTechniqueRef]: return [ t for t in self.techniques if t.status not in ("validated", "partial") ] @property def coverage_pct(self) -> float: if not self.techniques: return 0.0 return round(len(self.covered_techniques) / len(self.techniques) * 100, 1) @classmethod def from_orm(cls, orm: Any) -> ThreatActorEntity: techs: list[ThreatActorTechniqueRef] = [] for tat in getattr(orm, "techniques", None) or []: technique = getattr(tat, "technique", None) techs.append(ThreatActorTechniqueRef( technique_id=tat.technique_id, mitre_id=getattr(technique, "mitre_id", None) if technique else None, name=getattr(technique, "name", None) if technique else None, status=( technique.status_global.value if technique and hasattr(technique.status_global, "value") else getattr(technique, "status_global", None) if technique else None ), usage_description=tat.usage_description, )) return cls( id=orm.id, name=orm.name, mitre_id=orm.mitre_id, aliases=orm.aliases or [], description=orm.description, country=orm.country, target_sectors=orm.target_sectors or [], target_regions=orm.target_regions or [], motivation=orm.motivation, sophistication=orm.sophistication, first_seen=orm.first_seen, last_seen=orm.last_seen, is_active=orm.is_active if orm.is_active is not None else True, techniques=techs, )