feat(ttp): E.1.6 per-lifter contracts — six TolerantTagger subclasses

This commit is contained in:
2026-05-01 06:31:31 -04:00
parent cb9d183c20
commit 208ffd8f4f
8 changed files with 248 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
"""Behavioral lifter — derives techniques from cross-event session signal.
Contract step E.1.6 of ``development/TTP_TAGGING.md``. Empty body.
Implementation phase reads ``AttackerBehavior`` rows assembled by the
profiler and emits techniques the rule engine cannot see (timing,
ordering, command-graph shape). Inherits :class:`TolerantTagger` so a
missing ``AttackerBehavior`` join silently returns ``[]`` — sibling
worker absence is the steady state, not an error.
"""
from __future__ import annotations
from decnet.ttp.base import TaggerEvent, TolerantTagger
from decnet.web.db.models.ttp import TTPTag
class BehavioralLifter(TolerantTagger):
name = "behavioral"
#: Session-level events triggering a behavior-graph lookup. The
#: lifter reads ``AttackerBehavior`` keyed on the session.
HANDLES = frozenset({"session"})
async def _tag_impl(self, event: TaggerEvent) -> list[TTPTag]:
return []
__all__ = ["BehavioralLifter"]

View File

@@ -0,0 +1,25 @@
"""Canary fingerprint lifter — browser-payload derived technique tagger.
Contract step E.1.6 of ``development/TTP_TAGGING.md``. Empty body.
Implementation phase reads canary-payload fingerprints (navigator
properties, canvas hashes, proxy/VPN leakage signatures) and emits
Discovery / Defense-Evasion techniques. The evidence shape is pinned
to :class:`~decnet.web.db.models.ttp.CanaryFingerprintEvidence`
(``metric`` + ``matched_signature``) — raw fingerprint blobs never
land in evidence.
"""
from __future__ import annotations
from decnet.ttp.base import TaggerEvent, TolerantTagger
from decnet.web.db.models.ttp import TTPTag
class CanaryFingerprintLifter(TolerantTagger):
name = "canary_fingerprint"
HANDLES = frozenset({"canary_fingerprint"})
async def _tag_impl(self, event: TaggerEvent) -> list[TTPTag]:
return []
__all__ = ["CanaryFingerprintLifter"]

View File

@@ -0,0 +1,24 @@
"""Credential lifter — credential-capture / reuse technique tagger.
Contract step E.1.6 of ``development/TTP_TAGGING.md``. Empty body.
Implementation phase reads ``Credential`` and ``CredentialReuse`` rows
populated by the reuse-correlator and emits Credential-Access /
Lateral-Movement techniques. Tolerates absence of the reuse-correlator
output by inheriting :class:`TolerantTagger` — the correlator is a
sibling worker, not a hard dependency.
"""
from __future__ import annotations
from decnet.ttp.base import TaggerEvent, TolerantTagger
from decnet.web.db.models.ttp import TTPTag
class CredentialLifter(TolerantTagger):
name = "credential"
HANDLES = frozenset({"credential"})
async def _tag_impl(self, event: TaggerEvent) -> list[TTPTag]:
return []
__all__ = ["CredentialLifter"]

View File

@@ -0,0 +1,25 @@
"""Email lifter — SMTP message-level technique tagger.
Contract step E.1.6 of ``development/TTP_TAGGING.md``. Empty body.
Implementation phase parses message-level SMTP signal (headers,
attachment hashes, body sha) and emits Initial-Access / Phishing
techniques. PII discipline (design doc "Hard parts §6") is enforced at
the *type* layer: :class:`~decnet.web.db.models.ttp.EmailEvidence`
intentionally has no fields for raw rcpt addresses or body bytes, so
this lifter cannot leak them even by accident.
"""
from __future__ import annotations
from decnet.ttp.base import TaggerEvent, TolerantTagger
from decnet.web.db.models.ttp import TTPTag
class EmailLifter(TolerantTagger):
name = "email"
HANDLES = frozenset({"email"})
async def _tag_impl(self, event: TaggerEvent) -> list[TTPTag]:
return []
__all__ = ["EmailLifter"]

View File

@@ -0,0 +1,26 @@
"""Identity lifter — cross-attacker identity-rollup tagger.
Contract step E.1.6 of ``development/TTP_TAGGING.md``. Empty body.
Implementation phase reads identity-formation events (the clusterer
publishing ``identity.formed``) and emits techniques that are only
visible at the identity scope, never per-attacker — for example,
infrastructure rotation or credential reuse across IPs that were
clustered into one identity. Tags carry ``identity_uuid`` and a NULL
``attacker_uuid`` per the design doc's "identity rollup" worked
example.
"""
from __future__ import annotations
from decnet.ttp.base import TaggerEvent, TolerantTagger
from decnet.web.db.models.ttp import TTPTag
class IdentityLifter(TolerantTagger):
name = "identity"
HANDLES = frozenset({"identity"})
async def _tag_impl(self, event: TaggerEvent) -> list[TTPTag]:
return []
__all__ = ["IdentityLifter"]

View File

@@ -0,0 +1,30 @@
"""Intel lifter — opportunistic third-party verdict translator.
Contract step E.1.6 of ``development/TTP_TAGGING.md``. Empty body.
Implementation phase reads ``AttackerIntel`` rows and translates
provider verdicts (AbuseIPDB categories, GreyNoise classification,
Feodo / ThreatFox membership) into ATT&CK technique tags with
confidence scaled by per-provider reliability.
The decoupling rule (design doc §"Decoupling: bus-driven, never a
hard dependency") is enforced statically by E.2.7: this module MUST
NOT import from ``decnet.intel.{abuseipdb,greynoise,feodo,threatfox}``.
Only ``decnet.web.db.models`` symbols are permitted.
"""
from __future__ import annotations
from decnet.ttp.base import TaggerEvent, TolerantTagger
from decnet.web.db.models.ttp import TTPTag
class IntelLifter(TolerantTagger):
name = "intel"
#: ``intel`` events are bus-published when an ``AttackerIntel`` row
#: is upserted; the lifter treats absence as the steady state.
HANDLES = frozenset({"intel"})
async def _tag_impl(self, event: TaggerEvent) -> list[TTPTag]:
return []
__all__ = ["IntelLifter"]