"""Threat actor domain entity with coverage analysis logic. Pure domain logic — no framework imports. """ # Enable future language features for compatibility from __future__ import annotations # Import uuid import uuid # Import dataclass, field from dataclasses from dataclasses import dataclass, field # Import TYPE_CHECKING from typing from typing import TYPE_CHECKING # Check: TYPE_CHECKING if TYPE_CHECKING: # Import ThreatActor as ThreatActorORM from app.models.threat_actor from app.models.threat_actor import ThreatActor as ThreatActorORM # Apply the @dataclass decorator @dataclass # Define class ThreatActorTechniqueRef class ThreatActorTechniqueRef: """Lightweight reference to a technique used by an actor.""" # technique_id: uuid.UUID technique_id: uuid.UUID # Assign mitre_id = None mitre_id: str | None = None # Assign name = None name: str | None = None # Assign status = None status: str | None = None # Assign usage_description = None usage_description: str | None = None # Apply the @dataclass decorator @dataclass # Define class ThreatActorEntity class ThreatActorEntity: """Pure domain representation of a MITRE ATT&CK threat actor (group). Aggregates references to the techniques the actor is known to use and provides coverage analysis properties. """ # name: str name: str # Assign id = None id: uuid.UUID | None = None # Assign mitre_id = None mitre_id: str | None = None # Assign aliases = field(default_factory=list) aliases: list[str] = field(default_factory=list) # Assign description = None description: str | None = None # Assign country = None country: str | None = None # Assign target_sectors = field(default_factory=list) target_sectors: list[str] = field(default_factory=list) # Assign target_regions = field(default_factory=list) target_regions: list[str] = field(default_factory=list) # Assign motivation = None motivation: str | None = None # Assign sophistication = None sophistication: str | None = None # Assign first_seen = None first_seen: str | None = None # Assign last_seen = None last_seen: str | None = None # Assign is_active = True is_active: bool = True # Assign techniques = field(default_factory=list) techniques: list[ThreatActorTechniqueRef] = field(default_factory=list) # Apply the @property decorator @property # Define function technique_count def technique_count(self) -> int: """Return the total number of techniques associated with this actor. Returns: int: Count of technique references. """ # Return len(self.techniques) return len(self.techniques) # Apply the @property decorator @property # Define function covered_techniques def covered_techniques(self) -> list[ThreatActorTechniqueRef]: """Return technique references whose coverage status is ``validated`` or ``partial``. Returns: list[ThreatActorTechniqueRef]: Subset of techniques considered covered. """ # Return [ return [ t for t in self.techniques if t.status in ("validated", "partial") ] # Apply the @property decorator @property # Define function uncovered_techniques def uncovered_techniques(self) -> list[ThreatActorTechniqueRef]: """Return technique references whose coverage status is neither ``validated`` nor ``partial``. Returns: list[ThreatActorTechniqueRef]: Subset of techniques not yet covered. """ # Return [ return [ t for t in self.techniques if t.status not in ("validated", "partial") ] # Apply the @property decorator @property # Define function coverage_pct def coverage_pct(self) -> float: """Return the percentage of the actor's techniques that are covered. Returns: float: A value from 0.0 to 100.0, rounded to one decimal place. Returns 0.0 when the actor has no associated techniques. """ # Check: not self.techniques if not self.techniques: # Return 0.0 return 0.0 # Return round(len(self.covered_techniques) / len(self.techniques) * 100, 1) return round(len(self.covered_techniques) / len(self.techniques) * 100, 1) # Apply the @classmethod decorator @classmethod # Define function from_orm def from_orm(cls, orm: ThreatActorORM) -> ThreatActorEntity: """Build a ThreatActorEntity from a SQLAlchemy ThreatActor model. Args: orm (ThreatActorORM): The ORM model instance to convert. Returns: ThreatActorEntity: A fully populated domain entity including technique references resolved from the ORM relationship. """ # Assign techs = [] techs: list[ThreatActorTechniqueRef] = [] # Iterate over getattr(orm, "techniques", None) or [] for tat in getattr(orm, "techniques", None) or []: # Assign technique = getattr(tat, "technique", None) technique = getattr(tat, "technique", None) # Call techs.append() techs.append(ThreatActorTechniqueRef( # Keyword argument: technique_id technique_id=tat.technique_id, # Keyword argument: mitre_id mitre_id=getattr(technique, "mitre_id", None) if technique else None, # Keyword argument: name name=getattr(technique, "name", None) if technique else None, # Keyword argument: status status=( technique.status_global.value if technique and hasattr(technique.status_global, "value") else getattr(technique, "status_global", None) if technique else None ), # Keyword argument: usage_description usage_description=tat.usage_description, )) # Return cls( return cls( # Keyword argument: id id=orm.id, # Keyword argument: name name=orm.name, # Keyword argument: mitre_id mitre_id=orm.mitre_id, # Keyword argument: aliases aliases=orm.aliases or [], # Keyword argument: description description=orm.description, # Keyword argument: country country=orm.country, # Keyword argument: target_sectors target_sectors=orm.target_sectors or [], # Keyword argument: target_regions target_regions=orm.target_regions or [], # Keyword argument: motivation motivation=orm.motivation, # Keyword argument: sophistication sophistication=orm.sophistication, # Keyword argument: first_seen first_seen=orm.first_seen, # Keyword argument: last_seen last_seen=orm.last_seen, # Keyword argument: is_active is_active=orm.is_active if orm.is_active is not None else True, # Keyword argument: techniques techniques=techs, )