feat(bounty): surface file drops and stored mail in the Vault
The Bounty Vault page only read from the Bounty table, but
inotifywait-captured file drops (event_type=file_captured) and SMTP
quarantined messages (event_type=message_stored) were only landing in
the Logs table. AttackerDetail's tabs queried logs directly, so they
showed up per-attacker but were invisible on the global Vault page.
Mirror both events into Bounty as bounty_type=artifact with
payload.kind ∈ {file, mail} so the existing dedup
(bounty_type, attacker_ip, payload) collapses repeats by sha256. Add an
ARTIFACTS segment to the Vault filter row, plus dedicated render
branches: file drops show orig_path + size + writer attribution; mail
shows subject + From + attachment count + size, with the Mail icon
distinguishing them from FileText for file drops.
Forward-only — existing logs stay where they are. A backfill pass would
be straightforward (read Log WHERE event_type IN ('file_captured',
'message_stored') and feed each row through _extract_bounty) but is out
of scope here.
This commit is contained in:
@@ -568,6 +568,52 @@ async def _extract_bounty(
|
||||
},
|
||||
})
|
||||
|
||||
# 12. Captured file drops + stored mail. The `file_captured` event
|
||||
# comes from inotifywait quarantines on SSH deckies; `message_stored`
|
||||
# comes from the SMTP template's DATA-commit handler. Both are
|
||||
# already what AttackerDetail's artifacts/mail tabs read; mirroring
|
||||
# them into the Bounty table makes the global Vault page show them
|
||||
# alongside credentials and fingerprints. Dedup on (bounty_type,
|
||||
# attacker_ip, payload); sha256 in the payload guarantees per-drop
|
||||
# uniqueness so repeat captures don't multiply rows.
|
||||
_evt = log_data.get("event_type")
|
||||
if _evt == "file_captured" and _fields.get("stored_as"):
|
||||
await repo.add_bounty({
|
||||
"decky": log_data.get("decky"),
|
||||
"service": log_data.get("service"),
|
||||
"attacker_ip": log_data.get("attacker_ip"),
|
||||
"bounty_type": "artifact",
|
||||
"payload": {
|
||||
"kind": "file",
|
||||
"stored_as": _fields.get("stored_as"),
|
||||
"sha256": _fields.get("sha256"),
|
||||
"size": _fields.get("size"),
|
||||
"orig_path": _fields.get("orig_path"),
|
||||
"attribution": _fields.get("attribution"),
|
||||
"writer_comm": _fields.get("writer_comm"),
|
||||
},
|
||||
})
|
||||
elif _evt == "message_stored" and _fields.get("stored_as"):
|
||||
await repo.add_bounty({
|
||||
"decky": log_data.get("decky"),
|
||||
"service": log_data.get("service"),
|
||||
"attacker_ip": log_data.get("attacker_ip"),
|
||||
"bounty_type": "artifact",
|
||||
"payload": {
|
||||
"kind": "mail",
|
||||
"stored_as": _fields.get("stored_as"),
|
||||
"sha256": _fields.get("sha256"),
|
||||
"size": _fields.get("size"),
|
||||
"subject": _fields.get("subject"),
|
||||
"from_hdr": _fields.get("from_hdr"),
|
||||
"to_hdr": _fields.get("to_hdr"),
|
||||
"mail_from": _fields.get("mail_from"),
|
||||
"rcpt_to": _fields.get("rcpt_to"),
|
||||
"attachment_count": _fields.get("attachment_count"),
|
||||
"content_type": _fields.get("content_type"),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
# ─── IP-leak detection (XFF / Forwarded / X-Real-IP / CDN variants) ──────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user