feat(ttp): E.3.15 UKC bridge — production phase-handoff edge fires
Add BaseRepository.list_ttp_decky_phases(identity_uuid) returning per-decky tag observations as (decky_id, tactic, created_at_ts) rows ordered by creation time. Rewrite from_identity_row() to project tactic → UKCPhase via tactic_to_ukc_phase and populate the four phase-handoff maps (first/last_phase_per_decky, first/last_seen_per_decky) so combined_campaign_weight finally lights up on real DB rows — not just synthetic fixtures. ConnectedComponentsCampaignClusterer.tick() pulls each active identity's per-decky phase observations before projecting features. Repo failures are non-fatal: a partial repo falls back to the empty phase-handoff signal (legacy behavior) so the worker stays up. tests/clustering/test_ttp_phase_handoff.py pins the production-row pair clearing CAMPAIGN_EDGE_THRESHOLD on a C2 → DISCOVERY hand-off — the trip-wire that says the whole project paid off. commands_by_phase_on_decky itself stays empty on the production path: it is consumed only by the synthetic-fixture similarity surface, and the phase-handoff edge does not use it. Synthetic fixtures still populate it directly via from_synthetic_identity.
This commit is contained in:
@@ -231,6 +231,50 @@ class TTPMixin(_MixinBase):
|
||||
for r in res.all()
|
||||
]
|
||||
|
||||
async def list_ttp_decky_phases(
|
||||
self, identity_uuid: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Per-decky tag observations for the UKC bridge (E.3.15).
|
||||
|
||||
Includes (a) tags directly anchored on this identity and
|
||||
(b) tags anchored on Attackers whose ``identity_id`` projects
|
||||
up to this identity — same scope as
|
||||
:meth:`list_techniques_by_identity`.
|
||||
"""
|
||||
async with self._session() as session:
|
||||
attacker_uuids_subq = (
|
||||
select(col(Attacker.uuid))
|
||||
.where(col(Attacker.identity_id) == identity_uuid)
|
||||
.scalar_subquery()
|
||||
)
|
||||
stmt: Any = (
|
||||
select(
|
||||
col(TTPTag.decky_id),
|
||||
col(TTPTag.tactic),
|
||||
col(TTPTag.created_at),
|
||||
)
|
||||
.where(
|
||||
(
|
||||
(col(TTPTag.identity_uuid) == identity_uuid)
|
||||
| (col(TTPTag.attacker_uuid).in_(attacker_uuids_subq))
|
||||
)
|
||||
& (col(TTPTag.decky_id).is_not(None))
|
||||
)
|
||||
.order_by(col(TTPTag.created_at))
|
||||
)
|
||||
res = await session.execute(stmt)
|
||||
return [
|
||||
{
|
||||
"decky_id": r.decky_id,
|
||||
"tactic": r.tactic,
|
||||
"created_at_ts": (
|
||||
r.created_at.timestamp()
|
||||
if r.created_at is not None else 0.0
|
||||
),
|
||||
}
|
||||
for r in res.all()
|
||||
]
|
||||
|
||||
async def list_distinct_techniques(self) -> list[TechniqueRollupRow]:
|
||||
"""Fleet-wide distinct-technique rollup with counts +
|
||||
most-recent-seen timestamps.
|
||||
|
||||
Reference in New Issue
Block a user