| name | detection-engineering |
| description | Implement and review deterministic per-source brute-force detection for LogHunter. |
| license | MIT |
| metadata | {"project":"loghunter-cli","domain":"detection"} |
Detection Engineering
Public contract
def detect_brute_force(
events: Iterable[AuthEvent],
*,
threshold: int = 5,
window_seconds: int = 60,
) -> DetectionResult: ...
Reject threshold or window values below one before consuming events. Consume normalized events
once; do not read files, parse raw lines, call the analyzer, or retain raw input.
Accepted rule semantics
- Only
AuthEventType.LOGIN_FAILED participates. Invalid-user failures count and contribute their
username. Standalone invalid-user and successful events do not count, clear, split, or lower state.
- Isolate every normalized IPv4/IPv6 source. Never perform DNS or combine address families.
- Relevant timestamps must be non-decreasing per source; equal timestamps and globally
non-chronological interleaving across sources are valid. Ignore nonfailure timestamps.
- Maintain a bounded deque of active failures per source. The window is inclusive: retain an oldest
event when
current - oldest <= window, and evict only while the difference is greater.
- End a source sequence only when pre-append eviction empties its deque. Finalize its candidate and
let the current failure start a new sequence; there is no cooldown.
- Keep one evolving strongest candidate per active sequence. Replace it for a larger active count
and for an equal peak's later qualifying window. Do not regress when old failures leave. Finalize
all remaining candidates at end of input.
- Snapshot the active deque count, first/last timestamps, and sorted unique case-preserved usernames.
Severity is Medium for T <= C < 2T, High for 2T <= C < 3T, and Critical for C >= 3T.
Sort finalized findings by explicit severity rank descending, attempts descending, last observation
descending using direct datetime comparison, canonical source text ascending, and first observation
ascending. Never order naive datetimes through timestamp().
Memory must remain O(s + w + f): per-source bounded active deques, source state, and finalized
findings only. Do not materialize the input or globally sort it.
Required tests
Cover below/at/above threshold; inclusive and outside-window boundaries; all severity boundaries;
IPv4, IPv6, and independent sources; invalid-user failure versus standalone invalid-user; successes;
unique sorted users; duplicate suppression; separated bursts; strongest and equal-peak replacement;
per-source chronology including equal timestamps and valid global interleaving; invalid configuration
before consumption; empty, generator, and single-use iterables; and complete deterministic ordering.