merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
68
decnet/profiler/phases.py
Normal file
68
decnet/profiler/phases.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Recon → exfil phase sequencing for DECNET attacker profiles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from decnet.correlation.parser import LogEvent
|
||||
from decnet.telemetry import traced as _traced
|
||||
|
||||
# Events that signal "recon" phase (scans, probes, auth attempts).
|
||||
_RECON_EVENT_TYPES: frozenset[str] = frozenset({
|
||||
"scan", "connection", "banner", "probe",
|
||||
"login_attempt", "auth", "auth_failure",
|
||||
})
|
||||
|
||||
# Events that signal "exfil" / action-on-objective phase.
|
||||
_EXFIL_EVENT_TYPES: frozenset[str] = frozenset({
|
||||
"download", "upload", "file_transfer", "data_exfil",
|
||||
"command", "exec", "query", "shell_input",
|
||||
})
|
||||
|
||||
# Fields carrying payload byte counts (for "large payload" detection).
|
||||
_PAYLOAD_SIZE_FIELDS: tuple[str, ...] = ("bytes", "size", "content_length")
|
||||
|
||||
|
||||
@_traced("profiler.phase_sequence")
|
||||
def phase_sequence(events: list[LogEvent]) -> dict[str, Any]:
|
||||
"""
|
||||
Derive recon→exfil phase transition info.
|
||||
|
||||
Returns:
|
||||
recon_end_ts : ISO timestamp of last recon-class event (or None)
|
||||
exfil_start_ts : ISO timestamp of first exfil-class event (or None)
|
||||
exfil_latency_s : seconds between them (None if not both present)
|
||||
large_payload_count: count of events whose *fields* report a payload
|
||||
≥ 1 MiB (heuristic for bulk data transfer)
|
||||
"""
|
||||
recon_end = None
|
||||
exfil_start = None
|
||||
large_payload_count = 0
|
||||
|
||||
for e in sorted(events, key=lambda x: x.timestamp):
|
||||
if e.event_type in _RECON_EVENT_TYPES:
|
||||
recon_end = e.timestamp
|
||||
elif e.event_type in _EXFIL_EVENT_TYPES and exfil_start is None:
|
||||
exfil_start = e.timestamp
|
||||
|
||||
for fname in _PAYLOAD_SIZE_FIELDS:
|
||||
raw = e.fields.get(fname)
|
||||
if raw is None:
|
||||
continue
|
||||
try:
|
||||
if int(raw) >= 1_048_576:
|
||||
large_payload_count += 1
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
latency: float | None = None
|
||||
if recon_end is not None and exfil_start is not None and exfil_start >= recon_end:
|
||||
latency = round((exfil_start - recon_end).total_seconds(), 3)
|
||||
|
||||
return {
|
||||
"recon_end_ts": recon_end.isoformat() if recon_end else None,
|
||||
"exfil_start_ts": exfil_start.isoformat() if exfil_start else None,
|
||||
"exfil_latency_s": latency,
|
||||
"large_payload_count": large_payload_count,
|
||||
}
|
||||
Reference in New Issue
Block a user