feat(refactor): PEP8, type annotations, docstrings and PyJWT security fix
This commit is contained in:
@@ -1,18 +1,34 @@
|
||||
"""Domain entity classes representing core business objects."""
|
||||
# Import CampaignEntity from app.domain.entities.campaign
|
||||
from app.domain.entities.campaign import CampaignEntity
|
||||
|
||||
# Import from app.domain.entities.compliance
|
||||
from app.domain.entities.compliance import (
|
||||
ComplianceControlEntity,
|
||||
ComplianceFrameworkEntity,
|
||||
ControlCoverageStatus,
|
||||
)
|
||||
|
||||
# Import TechniqueEntity from app.domain.entities.technique
|
||||
from app.domain.entities.technique import TechniqueEntity
|
||||
|
||||
# Import ThreatActorEntity, ThreatActorTechniqueRef from app.domain.entities.threat_actor
|
||||
from app.domain.entities.threat_actor import ThreatActorEntity, ThreatActorTechniqueRef
|
||||
|
||||
# Assign __all__ = [
|
||||
__all__ = [
|
||||
# Literal argument value
|
||||
"CampaignEntity",
|
||||
# Literal argument value
|
||||
"ComplianceControlEntity",
|
||||
# Literal argument value
|
||||
"ComplianceFrameworkEntity",
|
||||
# Literal argument value
|
||||
"ControlCoverageStatus",
|
||||
# Literal argument value
|
||||
"TechniqueEntity",
|
||||
# Literal argument value
|
||||
"ThreatActorEntity",
|
||||
# Literal argument value
|
||||
"ThreatActorTechniqueRef",
|
||||
]
|
||||
|
||||
@@ -3,30 +3,59 @@
|
||||
Pure domain logic — no framework imports.
|
||||
"""
|
||||
|
||||
# Enable future language features for compatibility
|
||||
from __future__ import annotations
|
||||
|
||||
# Import enum
|
||||
import enum
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# Import uuid
|
||||
import uuid
|
||||
|
||||
# Import dataclass, field from dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Import TYPE_CHECKING from typing
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Import BusinessRuleViolation, InvalidStateTransition from app.domain.errors
|
||||
from app.domain.errors import BusinessRuleViolation, InvalidStateTransition
|
||||
|
||||
# Check: TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
# Import Campaign as CampaignORM from app.models.campaign
|
||||
from app.models.campaign import Campaign as CampaignORM
|
||||
|
||||
|
||||
# Define class CampaignStatus
|
||||
class CampaignStatus(str, enum.Enum):
|
||||
"""Lifecycle states for a campaign."""
|
||||
|
||||
# Assign draft = "draft"
|
||||
draft = "draft"
|
||||
# Assign active = "active"
|
||||
active = "active"
|
||||
# Assign completed = "completed"
|
||||
completed = "completed"
|
||||
# Assign archived = "archived"
|
||||
archived = "archived"
|
||||
|
||||
|
||||
# Define class CampaignType
|
||||
class CampaignType(str, enum.Enum):
|
||||
"""Classification of the campaign's testing methodology."""
|
||||
|
||||
# Assign custom = "custom"
|
||||
custom = "custom"
|
||||
# Assign apt_emulation = "apt_emulation"
|
||||
apt_emulation = "apt_emulation"
|
||||
# Assign kill_chain = "kill_chain"
|
||||
kill_chain = "kill_chain"
|
||||
# Assign compliance = "compliance"
|
||||
compliance = "compliance"
|
||||
|
||||
|
||||
# Assign VALID_TRANSITIONS = {
|
||||
VALID_TRANSITIONS: dict[CampaignStatus, list[CampaignStatus]] = {
|
||||
CampaignStatus.draft: [CampaignStatus.active],
|
||||
CampaignStatus.active: [CampaignStatus.completed],
|
||||
@@ -35,69 +64,156 @@ VALID_TRANSITIONS: dict[CampaignStatus, list[CampaignStatus]] = {
|
||||
}
|
||||
|
||||
|
||||
# Apply the @dataclass decorator
|
||||
@dataclass
|
||||
# Define class CampaignEntity
|
||||
class CampaignEntity:
|
||||
"""Pure domain representation of a security testing campaign.
|
||||
|
||||
Owns all lifecycle state-machine logic for campaign activation,
|
||||
completion, and archival.
|
||||
"""
|
||||
|
||||
# name: str
|
||||
name: str
|
||||
# Assign type = CampaignType.custom
|
||||
type: CampaignType = CampaignType.custom
|
||||
# Assign status = CampaignStatus.draft
|
||||
status: CampaignStatus = CampaignStatus.draft
|
||||
# Assign id = None
|
||||
id: uuid.UUID | None = None
|
||||
# Assign description = None
|
||||
description: str | None = None
|
||||
# Assign threat_actor_id = None
|
||||
threat_actor_id: uuid.UUID | None = None
|
||||
# Assign created_by = None
|
||||
created_by: uuid.UUID | None = None
|
||||
# Assign target_platform = None
|
||||
target_platform: str | None = None
|
||||
# Assign tags = field(default_factory=list)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
# Assign test_count = 0
|
||||
test_count: int = 0
|
||||
|
||||
# Define function can_transition_to
|
||||
def can_transition_to(self, target: CampaignStatus) -> bool:
|
||||
"""Check whether transitioning from the current status to *target* is valid.
|
||||
|
||||
Args:
|
||||
target (CampaignStatus): The desired next status.
|
||||
|
||||
Returns:
|
||||
bool: True if the transition is allowed, False otherwise.
|
||||
"""
|
||||
# Return target in VALID_TRANSITIONS.get(self.status, [])
|
||||
return target in VALID_TRANSITIONS.get(self.status, [])
|
||||
|
||||
# Define function activate
|
||||
def activate(self) -> None:
|
||||
"""Transition the campaign from ``draft`` to ``active``.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Check: not self.can_transition_to(CampaignStatus.active)
|
||||
if not self.can_transition_to(CampaignStatus.active):
|
||||
# Raise InvalidStateTransition
|
||||
raise InvalidStateTransition(
|
||||
self.status.value, CampaignStatus.active.value,
|
||||
[s.value for s in VALID_TRANSITIONS[self.status]],
|
||||
)
|
||||
# Check: self.test_count == 0
|
||||
if self.test_count == 0:
|
||||
# Raise BusinessRuleViolation
|
||||
raise BusinessRuleViolation(
|
||||
# Literal argument value
|
||||
"Campaign must have at least one test to activate"
|
||||
)
|
||||
# Assign self.status = CampaignStatus.active
|
||||
self.status = CampaignStatus.active
|
||||
|
||||
# Define function complete
|
||||
def complete(self) -> None:
|
||||
"""Transition the campaign from ``active`` to ``completed``.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Check: not self.can_transition_to(CampaignStatus.completed)
|
||||
if not self.can_transition_to(CampaignStatus.completed):
|
||||
# Raise InvalidStateTransition
|
||||
raise InvalidStateTransition(
|
||||
self.status.value, CampaignStatus.completed.value,
|
||||
[s.value for s in VALID_TRANSITIONS[self.status]],
|
||||
)
|
||||
# Assign self.status = CampaignStatus.completed
|
||||
self.status = CampaignStatus.completed
|
||||
|
||||
# Define function archive
|
||||
def archive(self) -> None:
|
||||
"""Transition the campaign from ``completed`` to ``archived``.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Check: not self.can_transition_to(CampaignStatus.archived)
|
||||
if not self.can_transition_to(CampaignStatus.archived):
|
||||
# Raise InvalidStateTransition
|
||||
raise InvalidStateTransition(
|
||||
self.status.value, CampaignStatus.archived.value,
|
||||
[s.value for s in VALID_TRANSITIONS[self.status]],
|
||||
)
|
||||
# Assign self.status = CampaignStatus.archived
|
||||
self.status = CampaignStatus.archived
|
||||
|
||||
# Define function ensure_modifiable
|
||||
def ensure_modifiable(self) -> None:
|
||||
"""Raise BusinessRuleViolation if the campaign is not in a modifiable state.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Check: self.status not in (CampaignStatus.draft, CampaignStatus.active)
|
||||
if self.status not in (CampaignStatus.draft, CampaignStatus.active):
|
||||
# Raise BusinessRuleViolation
|
||||
raise BusinessRuleViolation(
|
||||
f"Cannot modify campaign in '{self.status.value}' state"
|
||||
)
|
||||
|
||||
# Apply the @classmethod decorator
|
||||
@classmethod
|
||||
def from_orm(cls, orm: Any) -> CampaignEntity:
|
||||
"""Build a CampaignEntity from a SQLAlchemy Campaign model."""
|
||||
# Define function from_orm
|
||||
def from_orm(cls, orm: CampaignORM) -> CampaignEntity:
|
||||
"""Build a CampaignEntity from a SQLAlchemy Campaign model.
|
||||
|
||||
Args:
|
||||
orm (CampaignORM): The SQLAlchemy Campaign ORM model instance.
|
||||
|
||||
Returns:
|
||||
CampaignEntity: A fully populated domain entity reflecting the ORM state.
|
||||
"""
|
||||
# Assign test_count = len(getattr(orm, "campaign_tests", None) or [])
|
||||
test_count = len(getattr(orm, "campaign_tests", None) or [])
|
||||
# Return cls(
|
||||
return cls(
|
||||
# Keyword argument: id
|
||||
id=orm.id,
|
||||
# Keyword argument: name
|
||||
name=orm.name,
|
||||
# Keyword argument: type
|
||||
type=CampaignType(orm.type) if orm.type else CampaignType.custom,
|
||||
# Keyword argument: status
|
||||
status=CampaignStatus(orm.status) if orm.status else CampaignStatus.draft,
|
||||
# Keyword argument: description
|
||||
description=orm.description,
|
||||
# Keyword argument: threat_actor_id
|
||||
threat_actor_id=orm.threat_actor_id,
|
||||
# Keyword argument: created_by
|
||||
created_by=orm.created_by,
|
||||
# Keyword argument: target_platform
|
||||
target_platform=orm.target_platform,
|
||||
# Keyword argument: tags
|
||||
tags=orm.tags or [],
|
||||
# Keyword argument: test_count
|
||||
test_count=test_count,
|
||||
)
|
||||
|
||||
@@ -3,68 +3,161 @@
|
||||
Pure domain logic — no framework imports.
|
||||
"""
|
||||
|
||||
# Enable future language features for compatibility
|
||||
from __future__ import annotations
|
||||
|
||||
# Import enum
|
||||
import enum
|
||||
|
||||
# Import uuid
|
||||
import uuid
|
||||
|
||||
# Import dataclass, field from dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
# Define class ControlCoverageStatus
|
||||
class ControlCoverageStatus(str, enum.Enum):
|
||||
"""Computed coverage level for a single compliance control."""
|
||||
|
||||
# Assign covered = "covered"
|
||||
covered = "covered"
|
||||
# Assign partially_covered = "partially_covered"
|
||||
partially_covered = "partially_covered"
|
||||
# Assign not_covered = "not_covered"
|
||||
not_covered = "not_covered"
|
||||
|
||||
|
||||
# Apply the @dataclass decorator
|
||||
@dataclass
|
||||
# Define class ComplianceControlEntity
|
||||
class ComplianceControlEntity:
|
||||
"""Pure domain representation of a single compliance framework control.
|
||||
|
||||
Derives its coverage status from the technique statuses associated
|
||||
with it via the ``technique_statuses`` list.
|
||||
"""
|
||||
|
||||
# control_id: str
|
||||
control_id: str
|
||||
# title: str
|
||||
title: str
|
||||
# Assign id = None
|
||||
id: uuid.UUID | None = None
|
||||
# Assign description = None
|
||||
description: str | None = None
|
||||
# Assign category = None
|
||||
category: str | None = None
|
||||
# Assign technique_statuses = field(default_factory=list)
|
||||
technique_statuses: list[str] = field(default_factory=list)
|
||||
|
||||
# Apply the @property decorator
|
||||
@property
|
||||
# Define function coverage_status
|
||||
def coverage_status(self) -> ControlCoverageStatus:
|
||||
"""Compute the coverage status for this control based on linked technique statuses.
|
||||
|
||||
Returns:
|
||||
ControlCoverageStatus: ``covered`` when all techniques are covered,
|
||||
``partially_covered`` when at least one is covered, and
|
||||
``not_covered`` when none are covered or the control has no techniques.
|
||||
"""
|
||||
# Check: not self.technique_statuses
|
||||
if not self.technique_statuses:
|
||||
# Return ControlCoverageStatus.not_covered
|
||||
return ControlCoverageStatus.not_covered
|
||||
# Assign covered_statuses = {"validated", "partial"}
|
||||
covered_statuses = {"validated", "partial"}
|
||||
# Assign covered = [s for s in self.technique_statuses if s in covered_statuses]
|
||||
covered = [s for s in self.technique_statuses if s in covered_statuses]
|
||||
# Check: len(covered) == len(self.technique_statuses)
|
||||
if len(covered) == len(self.technique_statuses):
|
||||
# Return ControlCoverageStatus.covered
|
||||
return ControlCoverageStatus.covered
|
||||
# Alternative: len(covered) > 0
|
||||
elif len(covered) > 0:
|
||||
# Return ControlCoverageStatus.partially_covered
|
||||
return ControlCoverageStatus.partially_covered
|
||||
# Return ControlCoverageStatus.not_covered
|
||||
return ControlCoverageStatus.not_covered
|
||||
|
||||
|
||||
# Apply the @dataclass decorator
|
||||
@dataclass
|
||||
# Define class ComplianceFrameworkEntity
|
||||
class ComplianceFrameworkEntity:
|
||||
"""Pure domain representation of a compliance framework (e.g. NIST 800-53, PCI-DSS).
|
||||
|
||||
Aggregates a collection of controls and provides aggregate coverage statistics.
|
||||
"""
|
||||
|
||||
# name: str
|
||||
name: str
|
||||
# Assign id = None
|
||||
id: uuid.UUID | None = None
|
||||
# Assign version = None
|
||||
version: str | None = None
|
||||
# Assign description = None
|
||||
description: str | None = None
|
||||
# Assign is_active = True
|
||||
is_active: bool = True
|
||||
# Assign controls = field(default_factory=list)
|
||||
controls: list[ComplianceControlEntity] = field(default_factory=list)
|
||||
|
||||
# Apply the @property decorator
|
||||
@property
|
||||
# Define function total_controls
|
||||
def total_controls(self) -> int:
|
||||
"""Return the total number of controls in this framework.
|
||||
|
||||
Returns:
|
||||
int: Count of all controls regardless of coverage status.
|
||||
"""
|
||||
# Return len(self.controls)
|
||||
return len(self.controls)
|
||||
|
||||
# Apply the @property decorator
|
||||
@property
|
||||
# Define function covered_controls
|
||||
def covered_controls(self) -> int:
|
||||
"""Return the number of fully covered controls in this framework.
|
||||
|
||||
Returns:
|
||||
int: Count of controls with ``ControlCoverageStatus.covered`` status.
|
||||
"""
|
||||
# Return sum(
|
||||
return sum(
|
||||
# Literal argument value
|
||||
1 for c in self.controls
|
||||
if c.coverage_status == ControlCoverageStatus.covered
|
||||
)
|
||||
|
||||
# Apply the @property decorator
|
||||
@property
|
||||
# Define function coverage_pct
|
||||
def coverage_pct(self) -> float:
|
||||
"""Return the percentage of controls that are fully covered.
|
||||
|
||||
Returns:
|
||||
float: A value from 0.0 to 100.0, rounded to one decimal place.
|
||||
Returns 0.0 when the framework has no controls.
|
||||
"""
|
||||
# Check: self.total_controls == 0
|
||||
if self.total_controls == 0:
|
||||
# Return 0.0
|
||||
return 0.0
|
||||
# Return round(self.covered_controls / self.total_controls * 100, 1)
|
||||
return round(self.covered_controls / self.total_controls * 100, 1)
|
||||
|
||||
# Define function get_gap_controls
|
||||
def get_gap_controls(self) -> list[ComplianceControlEntity]:
|
||||
"""Return controls that are not fully covered.
|
||||
|
||||
Returns:
|
||||
list[ComplianceControlEntity]: Controls with ``partially_covered`` or
|
||||
``not_covered`` status.
|
||||
"""
|
||||
# Return [
|
||||
return [
|
||||
c for c in self.controls
|
||||
if c.coverage_status != ControlCoverageStatus.covered
|
||||
|
||||
@@ -12,105 +12,211 @@ Usage::
|
||||
entity.apply_to(technique_orm_model)
|
||||
"""
|
||||
|
||||
# Enable future language features for compatibility
|
||||
from __future__ import annotations
|
||||
|
||||
# Import uuid
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
# Import dataclass, field from dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Import datetime from datetime
|
||||
from datetime import datetime
|
||||
|
||||
# Import TYPE_CHECKING from typing
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Import TechniqueStatus, TestResult, TestState from app.domain.enums
|
||||
from app.domain.enums import TechniqueStatus, TestResult, TestState
|
||||
|
||||
# Import MitreId from app.domain.value_objects.mitre_id
|
||||
from app.domain.value_objects.mitre_id import MitreId
|
||||
|
||||
# Check: TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
# Import Technique as TechniqueORM from app.models.technique
|
||||
from app.models.technique import Technique as TechniqueORM
|
||||
|
||||
|
||||
# Apply the @dataclass decorator
|
||||
@dataclass(frozen=True)
|
||||
# Define class _TestSnapshot
|
||||
class _TestSnapshot:
|
||||
"""Minimal read-only view of a test for status calculation."""
|
||||
|
||||
# state: TestState
|
||||
state: TestState
|
||||
# detection_result: str | None
|
||||
detection_result: str | None
|
||||
|
||||
|
||||
# Apply the @dataclass decorator
|
||||
@dataclass
|
||||
# Define class TechniqueEntity
|
||||
class TechniqueEntity:
|
||||
"""Pure domain representation of a MITRE ATT&CK technique."""
|
||||
|
||||
# id: uuid.UUID
|
||||
id: uuid.UUID
|
||||
# mitre_id: str
|
||||
mitre_id: str
|
||||
# name: str
|
||||
name: str
|
||||
# Assign tactic = None
|
||||
tactic: str | None = None
|
||||
# Assign description = None
|
||||
description: str | None = None
|
||||
# Assign platforms = field(default_factory=list)
|
||||
platforms: list[str] = field(default_factory=list)
|
||||
# Assign is_subtechnique = False
|
||||
is_subtechnique: bool = False
|
||||
# Assign parent_mitre_id = None
|
||||
parent_mitre_id: str | None = None
|
||||
# Assign status_global = TechniqueStatus.not_evaluated
|
||||
status_global: TechniqueStatus = TechniqueStatus.not_evaluated
|
||||
# Assign review_required = False
|
||||
review_required: bool = False
|
||||
# Assign last_review_date = None
|
||||
last_review_date: datetime | None = None
|
||||
# Assign mitre_version = None
|
||||
mitre_version: str | None = None
|
||||
# Assign mitre_last_modified = None
|
||||
mitre_last_modified: datetime | None = None
|
||||
|
||||
# -- Factory -----------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
# Define function create
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
# Entry: mitre_id
|
||||
mitre_id: str,
|
||||
# Entry: name
|
||||
name: str,
|
||||
# Entry: tactic
|
||||
tactic: str | None = None,
|
||||
# Entry: description
|
||||
description: str | None = None,
|
||||
# Entry: platforms
|
||||
platforms: list[str] | None = None,
|
||||
) -> TechniqueEntity:
|
||||
"""Create a new technique, validating the MITRE ID format."""
|
||||
"""Create a new technique, validating the MITRE ID format.
|
||||
|
||||
Args:
|
||||
mitre_id (str): MITRE ATT&CK identifier (e.g. ``"T1059"`` or ``"T1059.001"``).
|
||||
name (str): Human-readable name of the technique.
|
||||
tactic (str | None): MITRE tactic category the technique belongs to.
|
||||
description (str | None): Optional free-text description.
|
||||
platforms (list[str] | None): List of platform strings the technique applies to.
|
||||
|
||||
Returns:
|
||||
TechniqueEntity: A new entity with a freshly generated UUID and
|
||||
``status_global`` set to ``not_evaluated``.
|
||||
"""
|
||||
# Assign validated_id = MitreId(mitre_id)
|
||||
validated_id = MitreId(mitre_id)
|
||||
# Return cls(
|
||||
return cls(
|
||||
# Keyword argument: id
|
||||
id=uuid.uuid4(),
|
||||
# Keyword argument: mitre_id
|
||||
mitre_id=validated_id.value,
|
||||
# Keyword argument: name
|
||||
name=name,
|
||||
# Keyword argument: tactic
|
||||
tactic=tactic,
|
||||
# Keyword argument: description
|
||||
description=description,
|
||||
# Keyword argument: platforms
|
||||
platforms=platforms or [],
|
||||
# Keyword argument: is_subtechnique
|
||||
is_subtechnique=validated_id.is_subtechnique,
|
||||
# Keyword argument: parent_mitre_id
|
||||
parent_mitre_id=validated_id.parent_id,
|
||||
# Keyword argument: status_global
|
||||
status_global=TechniqueStatus.not_evaluated,
|
||||
)
|
||||
|
||||
# Apply the @classmethod decorator
|
||||
@classmethod
|
||||
def from_orm(cls, model: Any) -> TechniqueEntity:
|
||||
"""Build a TechniqueEntity from a SQLAlchemy Technique model."""
|
||||
# Define function from_orm
|
||||
def from_orm(cls, model: TechniqueORM) -> TechniqueEntity:
|
||||
"""Build a TechniqueEntity from a SQLAlchemy Technique model.
|
||||
|
||||
Args:
|
||||
model (TechniqueORM): The ORM model instance to convert.
|
||||
|
||||
Returns:
|
||||
TechniqueEntity: A fully populated domain entity reflecting the ORM state.
|
||||
"""
|
||||
# Assign raw_status = model.status_global
|
||||
raw_status = model.status_global
|
||||
# Check: raw_status is None
|
||||
if raw_status is None:
|
||||
# Assign status = TechniqueStatus.not_evaluated
|
||||
status = TechniqueStatus.not_evaluated
|
||||
# Alternative: isinstance(raw_status, TechniqueStatus)
|
||||
elif isinstance(raw_status, TechniqueStatus):
|
||||
# Assign status = raw_status
|
||||
status = raw_status
|
||||
# Fallback: handle remaining cases
|
||||
else:
|
||||
# Assign status = TechniqueStatus(raw_status)
|
||||
status = TechniqueStatus(raw_status)
|
||||
# Return cls(
|
||||
return cls(
|
||||
# Keyword argument: id
|
||||
id=model.id,
|
||||
# Keyword argument: mitre_id
|
||||
mitre_id=model.mitre_id,
|
||||
# Keyword argument: name
|
||||
name=model.name,
|
||||
# Keyword argument: tactic
|
||||
tactic=model.tactic,
|
||||
# Keyword argument: description
|
||||
description=model.description,
|
||||
# Keyword argument: platforms
|
||||
platforms=model.platforms or [],
|
||||
# Keyword argument: is_subtechnique
|
||||
is_subtechnique=model.is_subtechnique or False,
|
||||
# Keyword argument: parent_mitre_id
|
||||
parent_mitre_id=model.parent_mitre_id,
|
||||
# Keyword argument: status_global
|
||||
status_global=status,
|
||||
# Keyword argument: review_required
|
||||
review_required=model.review_required or False,
|
||||
# Keyword argument: last_review_date
|
||||
last_review_date=model.last_review_date,
|
||||
# Keyword argument: mitre_version
|
||||
mitre_version=getattr(model, "mitre_version", None),
|
||||
# Keyword argument: mitre_last_modified
|
||||
mitre_last_modified=getattr(model, "mitre_last_modified", None),
|
||||
)
|
||||
|
||||
def apply_to(self, model: Any) -> None:
|
||||
"""Copy mutable fields back onto the ORM model."""
|
||||
# Define function apply_to
|
||||
def apply_to(self, model: TechniqueORM) -> None:
|
||||
"""Copy mutable fields back onto the ORM model.
|
||||
|
||||
Args:
|
||||
model (TechniqueORM): The ORM model to update in-place.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Assign model.status_global = self.status_global
|
||||
model.status_global = self.status_global
|
||||
# Assign model.review_required = self.review_required
|
||||
model.review_required = self.review_required
|
||||
# Assign model.last_review_date = self.last_review_date
|
||||
model.last_review_date = self.last_review_date
|
||||
|
||||
# -- Business logic ----------------------------------------------------
|
||||
|
||||
def recalculate_status(
|
||||
self,
|
||||
# Entry: test_snapshots
|
||||
test_snapshots: list[tuple[str, str | None]],
|
||||
) -> TechniqueStatus:
|
||||
"""Recompute ``status_global`` from a list of (state, detection_result) pairs.
|
||||
@@ -131,23 +237,37 @@ class TechniqueEntity:
|
||||
With only 1 validated+detected test the technique is "partial" to
|
||||
signal that more testing is recommended.
|
||||
|
||||
Returns the new status (also set on the entity).
|
||||
Args:
|
||||
test_snapshots (list[tuple[str, str | None]]): Each element is a
|
||||
``(state, detection_result)`` pair where *state* is a
|
||||
:class:`TestState` value string and *detection_result* is a
|
||||
:class:`TestResult` value string or ``None``.
|
||||
|
||||
Returns:
|
||||
TechniqueStatus: The newly computed status, which is also stored on
|
||||
the entity's ``status_global`` field.
|
||||
"""
|
||||
_MIN_VALIDATED_FOR_FULL = 2 # require ≥ N validated tests for "validated"
|
||||
|
||||
tests = [
|
||||
_TestSnapshot(
|
||||
# Keyword argument: state
|
||||
state=s if isinstance(s, TestState) else TestState(s),
|
||||
# Keyword argument: detection_result
|
||||
detection_result=dr,
|
||||
)
|
||||
for s, dr in test_snapshots
|
||||
]
|
||||
|
||||
# Check: not tests
|
||||
if not tests:
|
||||
# Assign self.status_global = TechniqueStatus.not_evaluated
|
||||
self.status_global = TechniqueStatus.not_evaluated
|
||||
# Alternative: all(t.state == TestState.validated for t in tests)
|
||||
elif all(t.state == TestState.validated for t in tests):
|
||||
validated_count = len(tests)
|
||||
results = [t.detection_result for t in tests if t.detection_result]
|
||||
# Check: results and all(r == TestResult.detected or r == "detected" for r i...
|
||||
if results and all(r == TestResult.detected or r == "detected" for r in results):
|
||||
# Need at least _MIN_VALIDATED_FOR_FULL tests for "validated"
|
||||
if validated_count >= _MIN_VALIDATED_FOR_FULL:
|
||||
@@ -155,24 +275,46 @@ class TechniqueEntity:
|
||||
else:
|
||||
self.status_global = TechniqueStatus.partial
|
||||
elif any(
|
||||
# Keyword argument: r
|
||||
r == TestResult.partially_detected or r == "partially_detected"
|
||||
for r in results
|
||||
):
|
||||
# Assign self.status_global = TechniqueStatus.partial
|
||||
self.status_global = TechniqueStatus.partial
|
||||
# Fallback: handle remaining cases
|
||||
else:
|
||||
# Assign self.status_global = TechniqueStatus.not_covered
|
||||
self.status_global = TechniqueStatus.not_covered
|
||||
# Alternative: any(t.state == TestState.validated for t in tests)
|
||||
elif any(t.state == TestState.validated for t in tests):
|
||||
# Assign self.status_global = TechniqueStatus.partial
|
||||
self.status_global = TechniqueStatus.partial
|
||||
# Fallback: handle remaining cases
|
||||
else:
|
||||
# Assign self.status_global = TechniqueStatus.in_progress
|
||||
self.status_global = TechniqueStatus.in_progress
|
||||
|
||||
# Return self.status_global
|
||||
return self.status_global
|
||||
|
||||
# Define function mark_reviewed
|
||||
def mark_reviewed(self) -> None:
|
||||
"""Mark the technique as reviewed, clearing the review flag."""
|
||||
"""Mark the technique as reviewed, clearing the review flag.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Assign self.review_required = False
|
||||
self.review_required = False
|
||||
# Assign self.last_review_date = datetime.utcnow()
|
||||
self.last_review_date = datetime.utcnow()
|
||||
|
||||
# Define function flag_for_review
|
||||
def flag_for_review(self) -> None:
|
||||
"""Flag the technique as needing review."""
|
||||
"""Flag the technique as needing review.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Assign self.review_required = True
|
||||
self.review_required = True
|
||||
|
||||
@@ -3,94 +3,204 @@
|
||||
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
|
||||
from typing import Any
|
||||
|
||||
# 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
|
||||
def from_orm(cls, orm: Any) -> ThreatActorEntity:
|
||||
# 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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user