feat(ttp): E.1.8 UKC bridge contract — ATTACK_TACTIC_TO_UKC + tactic_to_ukc_phase + inverse
This commit is contained in:
@@ -106,3 +106,71 @@ def stage_of(phase: UKCPhase) -> str:
|
||||
if phase in STAGE_THROUGH:
|
||||
return "through"
|
||||
return "out"
|
||||
|
||||
|
||||
# MITRE ATT&CK tactic ID -> UKC phase. Covers the 14 enterprise tactics
|
||||
# plus the four ICS tactics referenced by Appendix A.7 (Conpot, MQTT).
|
||||
# Adding additional ICS tactics is a one-line addition. See
|
||||
# TTP_TAGGING.md "UKC bridge".
|
||||
ATTACK_TACTIC_TO_UKC: dict[str, UKCPhase] = {
|
||||
# Enterprise
|
||||
"TA0043": UKCPhase.RECONNAISSANCE, # Reconnaissance
|
||||
"TA0042": UKCPhase.RESOURCE_DEVELOPMENT, # Resource Development
|
||||
"TA0001": UKCPhase.DELIVERY, # Initial Access
|
||||
"TA0002": UKCPhase.EXECUTION, # Execution
|
||||
"TA0003": UKCPhase.PERSISTENCE, # Persistence
|
||||
"TA0004": UKCPhase.PRIVILEGE_ESCALATION, # Privilege Escalation
|
||||
"TA0005": UKCPhase.DEFENSE_EVASION, # Defense Evasion
|
||||
"TA0006": UKCPhase.CREDENTIAL_ACCESS, # Credential Access
|
||||
"TA0007": UKCPhase.DISCOVERY, # Discovery
|
||||
"TA0008": UKCPhase.LATERAL_MOVEMENT, # Lateral Movement
|
||||
"TA0009": UKCPhase.COLLECTION, # Collection
|
||||
"TA0011": UKCPhase.COMMAND_AND_CONTROL, # Command and Control
|
||||
"TA0010": UKCPhase.EXFILTRATION, # Exfiltration
|
||||
"TA0040": UKCPhase.IMPACT, # Impact
|
||||
# ICS — first-class projection so MQTT / Conpot / Modbus tags
|
||||
# don't drop out of campaign rollups when the clusterer projects
|
||||
# tactic to phase. ICS uses an independent tactic-ID range.
|
||||
"TA0100": UKCPhase.COLLECTION, # ICS: Collection
|
||||
"TA0102": UKCPhase.DISCOVERY, # ICS: Discovery
|
||||
"TA0105": UKCPhase.IMPACT, # ICS: Impact
|
||||
"TA0106": UKCPhase.IMPACT, # ICS: Impair Process Control
|
||||
}
|
||||
|
||||
|
||||
def tactic_to_ukc_phase(tactic: str) -> UKCPhase | None:
|
||||
"""Map an ATT&CK tactic ID (e.g. ``"TA0001"``) to a :class:`UKCPhase`.
|
||||
|
||||
Returns ``None`` for unknown tactics. The map is closed-over the
|
||||
enterprise + ICS tactics referenced by the rule pack; a tactic
|
||||
outside that set is a contributor bug, not a runtime miss.
|
||||
"""
|
||||
return ATTACK_TACTIC_TO_UKC.get(tactic)
|
||||
|
||||
|
||||
# Inverse map, built once at import time. Several enterprise tactics
|
||||
# would collide (e.g. both TA0009 and TA0100 map to COLLECTION); the
|
||||
# enterprise tactic wins because it's listed first in
|
||||
# ATTACK_TACTIC_TO_UKC, which dict comprehension preserves via
|
||||
# last-write semantics — so we iterate in reverse to keep the FIRST
|
||||
# occurrence per phase. Pre-target phases (RECONNAISSANCE,
|
||||
# RESOURCE_DEVELOPMENT, WEAPONIZATION, SOCIAL_ENGINEERING) that are
|
||||
# not in OBSERVABLE_PHASES are deliberately lossy on the inverse —
|
||||
# TTP tags must never assign them, so projecting back to a tactic
|
||||
# is undefined. See TTP_TAGGING.md §UKC bridge.
|
||||
_UKC_TO_TACTIC: dict[UKCPhase, str] = {
|
||||
phase: tactic
|
||||
for tactic, phase in reversed(list(ATTACK_TACTIC_TO_UKC.items()))
|
||||
}
|
||||
|
||||
|
||||
def ukc_phase_to_tactic(phase: UKCPhase) -> str | None:
|
||||
"""Map a :class:`UKCPhase` back to an ATT&CK tactic ID.
|
||||
|
||||
Lossy on phases outside :data:`OBSERVABLE_PHASES` — pre-target
|
||||
phases (e.g. ``RECONNAISSANCE``, ``WEAPONIZATION``) return
|
||||
``None`` because no rule emits them, so the inverse is
|
||||
undefined by design. The CDD test in E.2.9 pins which phases
|
||||
are lossy.
|
||||
"""
|
||||
return _UKC_TO_TACTIC.get(phase)
|
||||
|
||||
@@ -2334,6 +2334,8 @@ unrelated events.
|
||||
|
||||
**E.1.8 — UKC bridge contract** (`decnet/clustering/ukc.py`)
|
||||
|
||||
**Status:** ✅ done.
|
||||
|
||||
- `ATTACK_TACTIC_TO_UKC: dict[str, UKCPhase]` — the static map
|
||||
from the body of this doc.
|
||||
- `def tactic_to_ukc_phase(tactic: str) -> UKCPhase | None`.
|
||||
|
||||
Reference in New Issue
Block a user