feat(domain): add domain layer foundation -- enums, value objects, TechniqueEntity, repository ports

This commit is contained in:
2026-02-18 19:10:31 +01:00
parent e651ef8a8c
commit 5c55e7c17f
14 changed files with 761 additions and 28 deletions

View File

@@ -0,0 +1,4 @@
from app.domain.value_objects.mitre_id import MitreId
from app.domain.value_objects.scoring_weights import ScoringWeights
__all__ = ["MitreId", "ScoringWeights"]

View File

@@ -0,0 +1,51 @@
"""MitreId — validated MITRE ATT&CK technique identifier.
Immutable value object that ensures the identifier follows the ATT&CK
format: ``T`` followed by 4 digits, optionally a dot and 3 more digits
for sub-techniques (e.g. ``T1059``, ``T1059.001``).
"""
from __future__ import annotations
import re
from dataclasses import dataclass
_MITRE_ID_RE = re.compile(r"^T\d{4}(\.\d{3})?$")
@dataclass(frozen=True, slots=True)
class MitreId:
"""Validated MITRE ATT&CK technique identifier."""
value: str
def __post_init__(self) -> None:
if not _MITRE_ID_RE.match(self.value):
raise ValueError(
f"Invalid MITRE ATT&CK ID '{self.value}'. "
"Expected format: T1234 or T1234.001"
)
@property
def is_subtechnique(self) -> bool:
return "." in self.value
@property
def parent_id(self) -> str | None:
"""Return the parent technique ID (e.g. T1059 for T1059.001)."""
if not self.is_subtechnique:
return None
return self.value.split(".")[0]
def __str__(self) -> str:
return self.value
def __eq__(self, other: object) -> bool:
if isinstance(other, MitreId):
return self.value == other.value
if isinstance(other, str):
return self.value == other
return NotImplemented
def __hash__(self) -> int:
return hash(self.value)

View File

@@ -0,0 +1,48 @@
"""ScoringWeights — validated immutable weight set for the scoring engine.
Enforces that all five weights are non-negative and sum to exactly 100.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class ScoringWeights:
"""Five scoring dimension weights that must sum to 100."""
tests: float
detection_rules: float
d3fend: float
freshness: float
platform_diversity: float
def __post_init__(self) -> None:
fields = [
self.tests,
self.detection_rules,
self.d3fend,
self.freshness,
self.platform_diversity,
]
for f in fields:
if f < 0:
raise ValueError("Scoring weights must be non-negative")
total = sum(fields)
if abs(total - 100) > 0.01:
raise ValueError(
f"Scoring weights must sum to 100, got {total}"
)
@classmethod
def default(cls) -> ScoringWeights:
"""Return the default weight distribution."""
return cls(
tests=40.0,
detection_rules=25.0,
d3fend=15.0,
freshness=10.0,
platform_diversity=10.0,
)