feat(ttp): implement E.3.14b intel catch-up via attacker.session.ended
On every attacker.session.ended event, the TTP worker now reads the persisted AttackerIntel row (if any) and synthesizes an intel-source TaggerEvent so intel-derived tags emit even when attacker.intel.enriched was dropped or arrived before the worker started. Key changes: - AttackerIntel.to_intel_event_payload() — single source of truth for the intel-row → lifter payload projection; shared by future callers without importing decnet.intel.* (no-SPOF contract preserved). - BaseRepository.get_attacker_intel_row_by_uuid() — returns the live SQLModel instance so the catch-up path can call to_intel_event_payload(). - _build_intel_catchup_event() in ttp/worker.py — looks up the intel row, builds the TaggerEvent, returns None on absent row (silence, not error). - _process_event() extended: appends the catch-up event to tagger_events when topic contains "session.ended". Deterministic source_id keeps compute_tag_uuid idempotent across replays; INSERT OR IGNORE deduplicates against any prior attacker.intel.enriched path. DummyRepo stub + coverage call added per feedback_run_base_repo_test.md.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Threat-intel enrichment row — one per attacker IP, TTL-cached."""
|
||||
import json as _json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlmodel import Field, SQLModel
|
||||
@@ -8,6 +9,18 @@ from sqlmodel import Field, SQLModel
|
||||
from ._base import _BIG_TEXT
|
||||
|
||||
|
||||
def _decode_json_list(value: Any) -> list[Any]:
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
if isinstance(value, str) and value:
|
||||
try:
|
||||
decoded = _json.loads(value)
|
||||
except (_json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
return decoded if isinstance(decoded, list) else []
|
||||
return []
|
||||
|
||||
|
||||
class AttackerIntel(SQLModel, table=True):
|
||||
"""Aggregated threat-intel verdict for a single attacker IP.
|
||||
|
||||
@@ -129,3 +142,46 @@ class AttackerIntel(SQLModel, table=True):
|
||||
default_factory=lambda: datetime.now(timezone.utc), index=True
|
||||
)
|
||||
expires_at: datetime = Field(index=True)
|
||||
|
||||
def to_intel_event_payload(
|
||||
self,
|
||||
*,
|
||||
providers: Optional[list[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Project this row into the payload shape the IntelLifter consumes.
|
||||
|
||||
Called by both the intel worker (on live publish of
|
||||
``attacker.intel.enriched``) and the TTP worker (on
|
||||
``attacker.session.ended`` catch-up). The two callers produce
|
||||
identical payloads for the same row, so IntelLifter tag UUIDs
|
||||
are deterministic regardless of which path delivered them.
|
||||
|
||||
``providers`` is included when the intel worker knows which
|
||||
providers contributed; the TTP catch-up path omits it (the
|
||||
IntelLifter does not predicate on ``providers``).
|
||||
"""
|
||||
d: dict[str, Any] = {
|
||||
"attacker_uuid": self.attacker_uuid,
|
||||
"attacker_ip": self.attacker_ip,
|
||||
"aggregate_verdict": self.aggregate_verdict,
|
||||
# AbuseIPDB
|
||||
"abuseipdb_score": self.abuseipdb_score,
|
||||
"abuseipdb_categories": _decode_json_list(self.abuseipdb_categories),
|
||||
# GreyNoise
|
||||
"greynoise_classification": self.greynoise_classification,
|
||||
"greynoise_name": self.greynoise_name,
|
||||
"greynoise_tags": _decode_json_list(self.greynoise_tags),
|
||||
# Feodo
|
||||
"feodo_listed": self.feodo_listed,
|
||||
"feodo_malware_family": self.feodo_malware_family,
|
||||
# ThreatFox
|
||||
"threatfox_listed": self.threatfox_listed,
|
||||
"threatfox_threat_types": _decode_json_list(self.threatfox_threat_types),
|
||||
"threatfox_ioc_types": _decode_json_list(self.threatfox_ioc_types),
|
||||
"threatfox_malware_families": _decode_json_list(
|
||||
self.threatfox_malware_families
|
||||
),
|
||||
}
|
||||
if providers is not None:
|
||||
d["providers"] = providers
|
||||
return d
|
||||
|
||||
Reference in New Issue
Block a user