feat(ttp/stix): fleet-wide STIX 2.1 export — GET /api/v1/attackers/export/stix

This commit is contained in:
2026-05-09 07:37:41 -04:00
parent f827197cc8
commit c210a56fc8
7 changed files with 299 additions and 0 deletions

View File

@@ -361,6 +361,41 @@ class TTPMixin(_MixinBase):
res = await session.execute(stmt)
return [r.model_dump(mode="json") for r in res.scalars().all()]
async def get_all_ttp_rollups_for_export(self) -> dict[str, list[dict[str, Any]]]:
"""Return ``{attacker_uuid: [rollup_dict, ...]}`` for all attackers.
Single query; used by the fleet STIX export so it doesn't fan out
N × list_techniques_by_attacker calls.
"""
async with self._session() as session:
stmt: Any = (
select(
col(TTPTag.attacker_uuid),
col(TTPTag.technique_id),
col(TTPTag.sub_technique_id),
func.max(col(TTPTag.tactic)).label("tactic"),
func.count().label("count"),
func.max(col(TTPTag.confidence)).label("confidence_max"),
)
.where(col(TTPTag.attacker_uuid).is_not(None))
.group_by(
TTPTag.attacker_uuid,
TTPTag.technique_id,
TTPTag.sub_technique_id,
)
)
res = await session.execute(stmt)
out: dict[str, list[dict[str, Any]]] = {}
for r in res.all():
out.setdefault(r.attacker_uuid, []).append({
"technique_id": r.technique_id,
"sub_technique_id": r.sub_technique_id,
"tactic": r.tactic,
"count": r.count,
"confidence_max": r.confidence_max,
})
return out
# ── Backfill iterators (E.4) ────────────────────────────────────
#
# Read-only iterators consumed by ``decnet ttp backfill`` to replay