feat(profiler): track SMTP victim domains per attacker
New SmtpTarget table records each (attacker, domain) pair observed via
the SMTP honeypots. Only the domain is stored — local-parts are dropped
at ingestion, so this table holds no user-identifying data beyond the
target organisation's identity.
The profiler worker extracts domains from rcpt_to / rcpt_denied /
message_accepted events, normalizes them (lowercase, strip local-part,
drop blocked TLDs), and upserts one row per pair with a running count +
first_seen / last_seen.
Three repo methods shipped:
* increment_smtp_target(attacker, domain) — upsert + bump
* list_smtp_targets(attacker) — per-attacker view
* smtp_target_seen(domain) — cross-attacker aggregate, shaped as the
federation-gossip RPC that V2 will expose.
The gossip-query shape is load-bearing: each operator can answer
"have any of your attackers targeted corp1.com?" without leaking
which attackers or when — the aggregate returns a bool + total count
+ first/last seen, nothing else.
This commit is contained in:
@@ -170,6 +170,35 @@ class BaseRepository(ABC):
|
||||
"""Retrieve the keystroke-dynamics profile row for a session."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def increment_smtp_target(self, attacker_uuid: str, domain: str) -> None:
|
||||
"""
|
||||
Record that ``attacker_uuid`` targeted ``domain`` via SMTP.
|
||||
|
||||
Upserts the (attacker_uuid, domain) row: inserts with count=1 +
|
||||
first_seen=now on first sight, bumps count + last_seen on every
|
||||
subsequent hit. Callers must pre-normalize ``domain`` (lowercase,
|
||||
local-part stripped).
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_smtp_targets(self, attacker_uuid: str) -> list[dict[str, Any]]:
|
||||
"""Return SmtpTarget rows for an attacker, ordered by most-recent first."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def smtp_target_seen(self, domain: str) -> dict[str, Any]:
|
||||
"""
|
||||
Cross-attacker aggregate for a victim domain.
|
||||
|
||||
Returns ``{seen: bool, count: int, first_seen: datetime|None,
|
||||
last_seen: datetime|None}``. Shaped as the federation-gossip RPC
|
||||
that V2 will expose — each operator can answer "have any of your
|
||||
attackers targeted this domain?" without leaking attacker identity.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_attacker_by_uuid(self, uuid: str) -> Optional[dict[str, Any]]:
|
||||
"""Retrieve a single attacker profile by UUID."""
|
||||
|
||||
Reference in New Issue
Block a user