refactor(db): split attackers.py into an attackers/ subpackage
Splits the 494-line attackers.py into five submixin files plus a
composing AttackersMixin in attackers/__init__.py:
_core.py (~95) Attacker CRUD + _deserialize_attacker
behavior.py (~110) AttackerBehavior + _deserialize_behavior
sessions.py (~50) SessionProfile read/write
smtp.py (~70) SmtpTarget per-attacker + cross-attacker views
activity.py (~190) log-derived activity (commands, leaks,
artifacts, stored mail, session log, transcripts)
IdentitiesMixin.list_observations_for_identity calls
self._deserialize_attacker; MRO resolves it onto AttackersCoreMixin
through the composed SQLModelRepository class.
This commit is contained in:
69
decnet/web/db/sqlmodel_repo/attackers/smtp.py
Normal file
69
decnet/web/db/sqlmodel_repo/attackers/smtp.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""SMTP victim-domain tracking (per-attacker counters and
|
||||
cross-attacker aggregate)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import desc, func, select
|
||||
|
||||
from decnet.web.db.models import SmtpTarget
|
||||
|
||||
|
||||
class SmtpTargetsMixin:
|
||||
async def increment_smtp_target(self, attacker_uuid: str, domain: str) -> None:
|
||||
"""Upsert an (attacker_uuid, domain) pair and bump count + last_seen.
|
||||
|
||||
Read-then-write under a single session — the UNIQUE constraint on
|
||||
(attacker_uuid, domain) guards against duplicate rows if the race
|
||||
ever materialises; we accept the ~1ms extra round-trip in exchange
|
||||
for a single dialect-portable implementation.
|
||||
"""
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
select(SmtpTarget)
|
||||
.where(SmtpTarget.attacker_uuid == attacker_uuid)
|
||||
.where(SmtpTarget.domain == domain)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
now = datetime.now(timezone.utc)
|
||||
if existing:
|
||||
existing.count += 1
|
||||
existing.last_seen = now
|
||||
session.add(existing)
|
||||
else:
|
||||
session.add(SmtpTarget(
|
||||
attacker_uuid=attacker_uuid,
|
||||
domain=domain,
|
||||
first_seen=now,
|
||||
last_seen=now,
|
||||
count=1,
|
||||
))
|
||||
await session.commit()
|
||||
|
||||
async def list_smtp_targets(self, attacker_uuid: str) -> list[dict[str, Any]]:
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
select(SmtpTarget)
|
||||
.where(SmtpTarget.attacker_uuid == attacker_uuid)
|
||||
.order_by(desc(SmtpTarget.last_seen))
|
||||
)
|
||||
return [r.model_dump(mode="json") for r in result.scalars().all()]
|
||||
|
||||
async def smtp_target_seen(self, domain: str) -> dict[str, Any]:
|
||||
"""Aggregate rows for this domain across every attacker in the DB."""
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
select(
|
||||
func.coalesce(func.sum(SmtpTarget.count), 0),
|
||||
func.min(SmtpTarget.first_seen),
|
||||
func.max(SmtpTarget.last_seen),
|
||||
).where(SmtpTarget.domain == domain)
|
||||
)
|
||||
total, first_seen, last_seen = result.one()
|
||||
return {
|
||||
"seen": int(total) > 0,
|
||||
"count": int(total),
|
||||
"first_seen": first_seen,
|
||||
"last_seen": last_seen,
|
||||
}
|
||||
Reference in New Issue
Block a user