feat(ttp): E.1.10 repository contract — TTPMixin with insert_tags + list_techniques_by_{identity,attacker,campaign,session} + list_distinct_techniques

Empty NotImplementedError bodies; the SQL lands at E.3 implementation.
Mixin composed onto SQLModelRepository alongside the existing domain
mixins. Dialect-specific INSERT-OR-IGNORE syntax overrides land in
the per-backend subclasses at E.3 per the dual-DB-backend convention.
This commit is contained in:
2026-05-01 07:21:37 -04:00
parent b7f206c8c5
commit b6e31e64e9
3 changed files with 110 additions and 0 deletions

View File

@@ -49,6 +49,7 @@ from decnet.web.db.sqlmodel_repo.realism import RealismMixin
from decnet.web.db.sqlmodel_repo.swarm import SwarmMixin
from decnet.web.db.sqlmodel_repo.topology import TopologyMixin
from decnet.web.db.sqlmodel_repo.tarpit import TarpitMixin
from decnet.web.db.sqlmodel_repo.ttp import TTPMixin
from decnet.web.db.sqlmodel_repo.webhooks import WebhooksMixin
@@ -69,6 +70,7 @@ class SQLModelRepository(
SwarmMixin,
TarpitMixin,
TopologyMixin,
TTPMixin,
WebhooksMixin,
BaseRepository,
):

View File

@@ -0,0 +1,106 @@
"""TTP-tagging repository — `ttp_tag` reads + idempotent inserts.
Contract step E.1.10 of `development/TTP_TAGGING.md`. Method bodies
raise ``NotImplementedError``; the SQL lands at E.3 implementation
phase. The shape — argument types, return types, idempotency
semantics on ``insert_tags`` — is the public contract from this
commit forward.
Per the dual-DB-backend project convention, dialect-specific behavior
(``INSERT OR IGNORE`` on SQLite vs ``INSERT IGNORE`` on MySQL) is
overridden in the per-dialect subclasses (``decnet.web.db.sqlite``,
``decnet.web.db.mysql``); the shared base lives here.
"""
from __future__ import annotations
from decnet.web.db.models import (
CampaignTechniqueRow,
IdentityTechniqueRow,
TechniqueRollupRow,
TTPTag,
)
from decnet.web.db.sqlmodel_repo._helpers import _MixinBase
class TTPMixin(_MixinBase):
"""Mixin: TTP-tag query + insert methods composed onto
:class:`SQLModelRepository`.
Expects ``self._session()`` from the base mixin. Adding a new
``ttp_tag`` query method here requires adding a contract test in
``tests/web/db/test_ttp_repo.py`` (E.2.13) AND a parametrized run
against both SQLite and MySQL via the existing ``db_backends``
fixture.
"""
async def insert_tags(self, rows: list[TTPTag]) -> int:
"""Bulk-upsert tags with ``INSERT OR IGNORE`` semantics.
Returns the number of rows actually inserted (i.e. that were
not already present at their deterministic
:func:`compute_tag_uuid` PK). The idempotency property is the
load-bearing contract: replaying the same source events must
converge to the same tag set without writing duplicates and
without raising. See TTP_TAGGING.md §"Idempotency" + §"Bus
topics — Loop-prevention invariant".
"""
raise NotImplementedError(
"insert_tags lands at E.3 implementation phase",
)
async def list_techniques_by_identity(
self,
uuid: str,
) -> list[IdentityTechniqueRow]:
"""Per-Identity TTP rollup. Joins ``ttp_tag`` on
``identity_uuid`` and groups by ``(technique_id,
sub_technique_id)``. Includes identity-rollup tags (with NULL
``attacker_uuid``) and per-event tags whose denormalised
``identity_uuid`` matches.
"""
raise NotImplementedError(
"list_techniques_by_identity lands at E.3",
)
async def list_techniques_by_attacker(
self,
uuid: str,
) -> list[IdentityTechniqueRow]:
"""Per-Attacker (per-IP) TTP rollup. Reads ``ttp_tag`` filtered
on ``attacker_uuid``. Identity-rollup tags (NULL attacker
anchor) are deliberately excluded — those belong to the
Identity, not any one IP underneath it.
"""
raise NotImplementedError(
"list_techniques_by_attacker lands at E.3",
)
async def list_techniques_by_campaign(
self,
uuid: str,
) -> list[CampaignTechniqueRow]:
"""Campaign-wide TTP rollup. Joins ``ttp_tag`` -> Identity ->
``campaign_uuid`` and groups across all member Identities.
"""
raise NotImplementedError(
"list_techniques_by_campaign lands at E.3",
)
async def list_techniques_by_session(
self,
sid: str,
) -> list[IdentityTechniqueRow]:
"""Session-scoped TTP timeline. Filtered on ``ttp_tag.session_id``.
Used by the SessionDetail page (post-v0).
"""
raise NotImplementedError(
"list_techniques_by_session lands at E.3",
)
async def list_distinct_techniques(self) -> list[TechniqueRollupRow]:
"""Fleet-wide distinct-technique rollup with counts +
most-recent-seen timestamps. Backs ``GET /api/v1/ttp/techniques``.
"""
raise NotImplementedError(
"list_distinct_techniques lands at E.3",
)

View File

@@ -2360,6 +2360,8 @@ unrelated events.
**E.1.10 — Repository contract** (`decnet/web/db/sqlmodel_repo/ttp.py`)
**Status:** ✅ done.
- `async def insert_tags(rows: list[TTPTag]) -> int` — bulk upsert
with `INSERT OR IGNORE` semantics for idempotency.
- `async def list_techniques_by_identity(uuid: str) -> list[...]`.