diff --git a/decnet/web/db/sqlmodel_repo/__init__.py b/decnet/web/db/sqlmodel_repo/__init__.py index df49a194..37568d26 100644 --- a/decnet/web/db/sqlmodel_repo/__init__.py +++ b/decnet/web/db/sqlmodel_repo/__init__.py @@ -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, ): diff --git a/decnet/web/db/sqlmodel_repo/ttp.py b/decnet/web/db/sqlmodel_repo/ttp.py new file mode 100644 index 00000000..8cac32a8 --- /dev/null +++ b/decnet/web/db/sqlmodel_repo/ttp.py @@ -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", + ) diff --git a/development/TTP_TAGGING.md b/development/TTP_TAGGING.md index 867ba297..2570f460 100644 --- a/development/TTP_TAGGING.md +++ b/development/TTP_TAGGING.md @@ -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[...]`.