fix(ttp): resolve attacker_uuid from attacker_ip on bus-event consume
The collector's `attacker.session.ended` envelope carries
`attacker_uuid: null` and `attacker_ip: <ip>` because the collector
doesn't talk to the DB. The TTP worker passed that null straight
through, and `TTPTag.__init__` raised the documented invariant:
ValueError: ttp_tag requires at least one of attacker_uuid /
identity_uuid; both NULL is not a valid anchor.
The worker now resolves `attacker_uuid` from `attacker_ip` via
`BaseRepository.get_attacker_uuid_by_ip` before fanning out the
event. When the IP isn't in the DB yet (profiler hasn't ingested
the row), the event is dropped with one log line — better than
exploding mid-tag.
- New `get_attacker_uuid_by_ip(ip) -> str | None` on the repo
(BaseRepository abstract + AttackersCoreMixin impl).
- `_resolve_attacker_uuid` helper in `decnet/ttp/worker.py` runs
before `_build_events`. Short-circuits when the payload already
has either anchor; drops the event when neither anchor is
resolvable.
- Tests pin: short-circuit on existing uuid/identity, repo lookup,
drop on unknown IP, drop on "Unknown" sentinel, drop on
no-anchor payload, drop on repo failure.
This commit is contained in:
@@ -382,6 +382,15 @@ class BaseRepository(ABC):
|
||||
"""Retrieve a single attacker profile by UUID."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_attacker_uuid_by_ip(self, ip: str) -> Optional[str]:
|
||||
"""Return the :class:`Attacker` UUID for *ip*, or ``None``.
|
||||
|
||||
Used by the TTP worker to resolve ``attacker_uuid`` from the
|
||||
``attacker_ip`` carried by collector-side bus events.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_attackers(
|
||||
self,
|
||||
|
||||
@@ -48,6 +48,20 @@ class AttackersCoreMixin(_MixinBase):
|
||||
await session.commit()
|
||||
return row_uuid
|
||||
|
||||
async def get_attacker_uuid_by_ip(self, ip: str) -> Optional[str]:
|
||||
"""Return the :class:`Attacker` UUID for *ip*, or ``None``.
|
||||
|
||||
Used by the TTP worker to resolve ``attacker_uuid`` from the
|
||||
``attacker_ip`` carried by collector-side bus events
|
||||
(``attacker.session.ended`` etc.). Cheaper than
|
||||
:meth:`get_attacker_by_uuid` because it scalars a single column.
|
||||
"""
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
select(col(Attacker.uuid)).where(Attacker.ip == ip)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_attacker_by_uuid(self, uuid: str) -> Optional[dict[str, Any]]:
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
|
||||
Reference in New Issue
Block a user