Most blue team tooling assumes you have a SIEM, a budget, and a network connection. That's a fine assumption for an enterprise SOC, but it kills the feedback loop for students, home-labbers, IR consultants who land in air-gapped environments, and anyone who just wants to triage a dump of logs without spinning up infrastructure.
That's the gap I built ThreatLens to fill: a single Python CLI that ingests EVTX, JSON, Syslog, and CEF logs, runs Sigma rules plus a set of opinionated detectors, correlates findings across stages of an attack, and produces alerts already mapped to MITRE ATT&CK. No agents, no cloud, no license keys. Just pip install threatlens and point it at a log file.
What it actually does
ThreatLens isn't trying to replace Splunk. It's the layer that runs before a SIEM — or instead of one when you don't have access to one. The detection coverage is what you'd expect a junior analyst to want pre-built:
- Brute-force and password spray with logic that distinguishes the two
- Lateral movement via rapid network logons across hosts
- Privilege escalation by watching sensitive privilege assignments
- Suspicious process execution (LOLBins, encoded PowerShell, certutil download cradles, SAM dumping)
- Defense evasion (log clearing, Defender disabled, audit policy tampering)
- Persistence (services, scheduled tasks, Run keys, startup folders)
- Discovery bursts (whoami, ipconfig, net, nltest, dsquery in rapid succession)
- Kerberoasting and AS-REP roasting
- Credential access including LSASS, SAM, and DCSync
- Attack chain correlation that links credential access → priv esc → lateral movement → execution into a single multi-stage alert
Each alert ships with the MITRE technique ID, the matching evidence, and a severity that's actually based on signal density instead of a flat "MEDIUM" stamp.
Why I built it
I'm a cybersecurity student grinding through CompTIA certs and trying to build muscle memory around log-based threat hunting. The problem is, the second you step outside a paid lab, the resources dry up. Hunting in a free Splunk trial means re-uploading data every week. ELK is a weekend of YAML before you see your first alert. Even paid SaaS SIEMs hide the detection logic behind a UI, which is great for production and terrible for learning.
I wanted a tool that made the detection logic the artifact. Open the repo, read detections/brute_force.py, and you can see exactly why a 4625 burst from a single IP becomes a "Brute-Force" alert and why the same IP hitting 30 different usernames becomes "Password Spray" instead. That transparency is the entire point.
A code walkthrough: the brute-force detector
The cleanest example of how the project is structured is the brute-force detector, because it's also the one that taught me the most. The naive version is "5 failed logons in 5 minutes, alert." That misses password spray (one attempt against many users) and over-fires on legitimate failures during outages.
Here's the core of the actual detector (source):
class BruteForceDetector(DetectionRule):
name = "Brute-Force / Password Spray"
mitre_tactic = "Credential Access"
mitre_technique = "T1110 - Brute Force"
def analyze(self, events: list[LogEvent]) -> list[Alert]:
failed = [e for e in events if e.event_id in FAILED_LOGON_IDS]
if not failed:
return []
alerts: list[Alert] = []
# Group by source IP
by_ip: dict[str, list[LogEvent]] = defaultdict(list)
for event in failed:
key = event.source_ip or "unknown"
by_ip[key].append(event)
for source, source_events in by_ip.items():
windows = find_dense_windows(
source_events, self.window_seconds, self.threshold
)
for window in windows:
if len(window) >= self.threshold:
targets = {e.target_username or e.username for e in window}
is_spray = len(targets) > 3
rule_label = "Password Spray" if is_spray else "Brute-Force"
...
A few things are happening that are worth pulling apart:
1. It groups by source first, not by user. Most beginner tutorials key on the username. That breaks the moment an attacker rotates usernames against the same source. Keying on source IP catches the spray pattern naturally.
2. The window logic is delegated. find_dense_windows is a small utility that finds the densest sub-windows inside a list of events sorted by timestamp. Pulling it into a utility means lateral movement, discovery, and credential access detectors all share the same primitive — the rules stay declarative.
3. Brute-force vs spray is one line. is_spray = len(targets) > 3. If a single source is hammering more than three distinct usernames inside the window, it's almost never a misconfigured client and almost always a spray. The threshold is configurable, but the default is calibrated against the public detection logic in the SOC Prime and Sigma communities.
4. Severity scales with density. The alert isn't just "found something." If the burst is 3x the threshold it ships as HIGH, 2x as MEDIUM, and so on. That keeps the noise floor sane when you point it at a noisy log.
The same shape repeats across the other detectors. Each one is ~100 lines, each one inherits from a tiny DetectionRule base class, and each one is unit tested with synthetic events so you can read the test file and see exactly what the rule fires on.
Multi-stage correlation
The detection that took the longest to get right is the attack chain correlator. Individually, a single 4625, a single privilege assignment, and a single network logon are all noise. Stitched together — credential access → priv esc → lateral movement, on the same actor, inside the same time window — they're a kill chain.
The correlator scans all alerts produced by the per-tactic detectors, groups them by actor (user or source IP), sorts by tactic-stage, and emits a single multi-stage alert when at least two ATT&CK tactics fire in sequence. The output is a timeline a human can actually read, not a wall of unrelated tickets.
That's the part that feels closest to real SOC work. You stop staring at a single event and start asking "what's the story?"
Output formats
ThreatLens ships three:
- Terminal — color-coded summary, severity counts, top techniques, exit code = severity tier (so you can chain it in CI)
-
JSON — full structured alerts, ready to feed into a SIEM, a webhook, or just
jq - HTML report — standalone, no JS framework, includes a severity chart and an interactive attack timeline. This is the format I keep open in a tab while I work through a dataset.
What's next
The roadmap I'm actually working on:
- More native parsers (currently leaning on
python-evtx; eventually want a pure-Python EVTX reader for environments where pip is restricted) - Better Sigma backend coverage so community rules drop in cleanly
- A small library of "hunt packs" — pre-built configurations targeted at specific scenarios (ransomware aftermath, lateral movement audit, post-phish triage)
Try it
If any of this sounds useful, the repo is here: github.com/TiltedLunar123/ThreatLens. There's a sample log in sample_data/ so you can run threatlens scan sample_data/sample_security_log.json and see real alerts in under 30 seconds.
A star helps the project show up for other students and early-career analysts trying to find tools that aren't locked behind enterprise pricing. PRs welcome — especially new detectors. If there's a tactic you've been wanting to hunt for and the repo doesn't cover it yet, open an issue and we'll talk through the rule design.
Top comments (0)