Threat Hunting
Definition
Section titled “Definition”Threat hunting is the proactive search for compromise that alerts missed — hypothesis-driven analysis over telemetry you already collect, executed by an analyst who assumes the intrusion is happening and sets out to find it. The doctrine that separates hunting from alert-watching: the hypothesis must be falsifiable (“attackers staging T1053.005 scheduled tasks outside change windows would appear as 4698 events from non-admin service contexts” — you can be wrong, and being wrong is a result), and the hunt must convert: every hunt ends in one of three outcomes — finding (real compromise or real exposure), new detection (the pattern was real; BT9 gets a rule and the hunt graduates), or telemetry gap (the hunt failed because the data doesn’t exist — BT3’s backlog, the most underrated output). David Bianco’s Hunting Maturity Model frames the program: HM1 (initial — alerts only) through HM5 (leading — automated data analysis feeding constant hunting); the honest journey is HM2 (minimal: periodic hunts from intel) → HM3 (procedural: repeatable methods documented) → HM4 (innovative: custom procedures per environment) — maturity is method, not tooling. The craft’s center: hunting exploits the attacker’s asymmetry in reverse — they must avoid all your eyes once; you need to look once in the right place — and the discipline is choosing where to look with a reason.
Why It Matters
Section titled “Why It Matters”Alerts catch what signatures predicted; hunting catches what your detection estate structurally cannot — novel tooling, living-off-the-land abuse (LOLBins), insider behavior, and the long-tail techniques whose signatures would never pay for their FP rate. The dwell-time reality justifies the posture: intrusions discovered by external parties (law enforcement, other victims, responsible disclosure) routinely dwelt for weeks or months — industry medians sit in the low double digits of days in recent M-Trends editions [verify current edition] — and every externally-discovered intrusion is an indictment of the local detection estate. Hunting is how that estate improves from the inside: each hunt either closes a real gap or produces the evidence that a detection is working.
The failure modes define the craft’s discipline. Stroll-hunting: opening the SIEM and clicking around without a hypothesis — produces dashboard screenshots, not findings. The unconverted hunt: a genuinely novel pattern found, written up, and never becoming a rule — three months later the same TTP lands again and the alert never fires; the hunt’s value evaporated at the handoff. Hunting past the telemetry: hypothesizing about syscall behavior when only 4688 command-line logging exists — the hunt’s real finding (declared honestly) is that BT3 needs a new source. Alert-chasing: re-investigating what BT1 already triaged and calling it hunting. And the hero model: one brilliant hunter whose method lives in their head — the program ends at their exit interview; HM3 exists precisely to convert brilliance into procedure.
Detailed Explanation
Section titled “Detailed Explanation” INPUTS (with a reason each) +-------------------+ BT10 intel: adversary TTPs targeting your sector | threat picture | BT9 gaps: techniques with no validated rule +-------------------+ BT4 RCAs: what surprised us last incident? | BT1 FP log: what triage wishes it could confirm? v +------------------+ | HYPOTHESIS | falsifiable, tied to a technique (T-id), | (hunthypo format)| names its telemetry, predicts an observable +--------+---------+ v +------------------+ data exists? --no--> TELEMETRY GAP | EXECUTE | | -> BT3 backlog (real output) | query, baseline, |--------+ | stack, outlier | | yes +--------+---------+ v | +------------------+ | | ANALYZE | baseline vs current, |----------->| rare? anomalous? | stack-count, peer compare | +--------+---------+ | | v v +--------------------------------------------------+ | OUTCOME (all three are wins) | | FINDING -> BT4 response, BT5 evidence | | DETECTION -> BT9 rule + validation, hunt retires | | GAP -> BT3 source request, hunt re-opens later | +--------------------------------------------------+Hypothesis formation draws from four inputs, each with a reason: the threat picture (BT10’s PIRs — which adversaries, which techniques), the coverage map (BT9’s five-state matrix — where validated coverage is weakest and the technique is likely), incident retrospectives (what surprised us — surprises are uninstrumented assumptions), and triage friction (what BT1 can’t confirm from alerts alone). A well-formed hypothesis names the ATT&CK technique, the telemetry source, the observable, and the baseline logic: “T1053.005 via non-admin contexts outside change windows, observed in Windows Security 4698, baseline = per-host monthly cadence, anomaly = first-ever or burst.”
Execution methods stack in increasing sophistication. Stack counting (frequency analysis): group by a field (process name × host, DNS query × subnet), look for the rare tail — the workhorse of rare-process hunting (rareproc below). Peer comparison: compare a host against its peer group (all SQL servers should look alike; the one running certutil -urlcache is the finding). Baseline drift: per-entity time-series against its own history (first-ever is the cheapest strong signal). Graph pivots: process → parent → network → file chains hunting the relationship patterns (Office spawning script interpreters — Sysmon EID 1 parent-child joins). LOLBin baselining: for the ~50 binaries attackers love (certutil, mshta, rundll32, bitsadmin…), baseline arguments not just names — rundll32 existing is noise; rundll32 javascript:"..." is the hunt.
Conversion is the program’s economic engine: the hunt-to-detection rate (BYOT’s core metric) tells you whether hunting is feeding the estate or generating essays. And telemetry gaps are wins with an asterisk: a hunt that cannot run because the data doesn’t exist has still produced the quarter’s most defensible BT3 request — “we hunted T1003.001 and could not; here is the source we need and what it costs.”
Deep dive: why the baseline journal matters more than the query
The quiet failure of most hunting programs is re-chasing: every week the same benign anomalies surface, burn the same two hours, and die in the same analyst’s memory — because the baseline annotations (that build server runs node.exe from its CI since March; those three hosts are the lab fleet; the CFO’s assistant bulk-downloads quarterly) lived in someone’s head or a notebook. The journal fixes the economics, not just the memory: a hunt’s marginal cost drops toward zero when its baseline notes persist, and the notes compound — six months of annotations turn a 40-candidate queue into a 6-candidate queue without changing any query. The discipline: every explainable candidate gets one journal line (what, where, why-benign, when-explained), and the journal is the first artifact a new hunter inherits. Tooling runs the query; the journal is what makes next week’s query worth running.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Rare-process hunting | Fleet-frequency tail analysis | Novel-malware arms race | Baselines decay; refresh |
| Stack counting | Frequency over grouped fields | High-cardinality noise | Pick the group smartly |
| Peer-group comparison | Host vs its role peers | Wrong peer group | SQL ≠ SQL ≠ SQL |
| First-ever baselines | Entity history vs now | Baseline drift, re-orgs | Rolling windows |
| LOLBin argument baseline | Argument patterns, not names | Encoding tricks | EID 1 cmdline depth |
| Parent-child graph | Office→script chains | Log correlation cost | EID 1 joins |
| DeTT&CT data mapping | Telemetry vs ATT&CK coverage | Mapping rot | Real tool, real output |
| Hunt→rule conversion | Findings become BT9 content | Unconverted hunts | The economic metric |
| Intel-driven hunts | BT10 PIRs → hypotheses | Generic threat chasing | Sector-specific only |
| Gap declaration | “Cannot hunt this” as output | Gaps seen as failure | BT3 backlog fuel |
Full Working Code
Section titled “Full Working Code”rareproc — the rare-process hunt as a tool: reads a process-launch export (CSV: host, user, process, cmdline, parent; from Sysmon EID 1 or 4688 pipelines), builds two baselines — per-host process history and fleet-wide process frequency — and flags the hunt candidates: processes never seen on this host, LOLBin launches with suspicious argument patterns, and process/parent pairs rare across the fleet. Outputs a scored hunt queue with the exact evidence lines.
#!/usr/bin/env python3"""rareproc — rare-process hunting over a process-launch export.
Input CSV columns: host, user, process, cmdline, parent (Sysmon EID 1 / Windows 4688 pipeline export; any window, 30d typical)Baseline: fleet-wide process frequency + per-host (host, process) history.Output: scored hunt queue (rareproc_out/hunt_queue.csv)."""import argparseimport csvimport sysfrom collections import Counter, defaultdictfrom pathlib import Path
LOLBINS = {"certutil", "mshta", "rundll32", "regsvr32", "bitsadmin", "installutil", "mavinject", "presentationhost", "workflowhost", "cscript", "wscript", "msxsl", "hh", "diskshadow"}SUSPICIOUS_ARGS = ("javascript:", "vbscript:", "-urlcache", "http", "downloadstring", "-encodedcommand", "frombase64", "/createnowrite", "localserver")# scored per hunt-candidate classW_NEVER_HOST = 3.0 # process never seen on this hostW_LOLBIN_ARGS = 4.0 # LOLBin with suspicious argumentsW_RARE_FLEET = 2.0 # fleet frequency in the rarest decileW_ODD_PARENT = 2.5 # (process, parent) pair rare fleet-wideREQUIRED = ("host", "user", "process", "cmdline", "parent")
def load(path: Path): if not path.exists(): sys.exit(f"fail-loud: export not found: {path}") with path.open(encoding="utf-8", errors="replace") as fh: reader = csv.DictReader(fh) missing = [c for c in REQUIRED if c not in (reader.fieldnames or [])] if missing: sys.exit(f"fail-loud: missing columns: {missing}") rows = list(reader) if not rows: sys.exit("fail-loud: zero events — pipeline broke, not a quiet month") return rows
def main(): ap = argparse.ArgumentParser(description="rare-process hunt") ap.add_argument("export", type=Path) ap.add_argument("--top", type=int, default=25, help="hunt queue size (default 25)") ap.add_argument("--out", type=Path, default=Path("rareproc_out")) args = ap.parse_args()
rows = load(args.export)
fleet = Counter(r["process"].strip().lower() for r in rows) host_seen = defaultdict(set) pair_count = Counter() for r in rows: proc = r["process"].strip().lower() host_seen[r["host"].strip()].add(proc) pair_count[(proc, r["parent"].strip().lower())] += 1
# fleet rarity: processes in the rarest 5% of launch counts counts = sorted(fleet.values()) rare_threshold = counts[max(0, int(len(counts) * 0.05))] if counts else 0 pair_freq = Counter(pair_count.values())
candidates = [] for r in rows: proc = r["process"].strip().lower() proc_base = Path(proc).name if "/" in proc or "\\" in proc else proc proc_base = proc_base.lower() host = r["host"].strip() score, reasons = 0.0, []
if proc_base not in host_seen[host]: score += W_NEVER_HOST reasons.append(f"never seen on {host}")
cmdline = r["cmdline"].lower() if proc_base in LOLBINS: hits = [s for s in SUSPICIOUS_ARGS if s in cmdline] if hits: score += W_LOLBIN_ARGS reasons.append(f"lolbin args: {','.join(hits[:3])}") elif proc_base.endswith((".ps1", ".vbs", ".js", ".hta")): score += 1.0 reasons.append("script artifact execution")
if fleet[proc_base] <= rare_threshold: score += W_RARE_FLEET reasons.append(f"fleet-rare ({fleet[proc_base]} launches)")
pair = (proc_base, r["parent"].strip().lower()) if pair_freq[pair_count[pair]] and pair_count[pair] <= 2: score += W_ODD_PARENT reasons.append(f"rare parent pair: {pair[1][:30]}")
if score > 0: candidates.append({"score": round(score, 2), "host": host, "user": r["user"].strip(), "process": proc_base, "parent": r["parent"].strip()[:40], "cmdline": r["cmdline"].strip()[:120], "reasons": "; ".join(reasons)})
candidates.sort(key=lambda c: -c["score"]) args.out.mkdir(parents=True, exist_ok=True) with (args.out / "hunt_queue.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=["score", "host", "user", "process", "parent", "cmdline", "reasons"]) w.writeheader() w.writerows(candidates[:args.top])
print(f"events={len(rows)} hosts={len(host_seen)} " f"distinct_procs={len(fleet)} rare_threshold={rare_threshold}") print(f"candidates={len(candidates)} queue={min(args.top, len(candidates))}") for c in candidates[:10]: print(f" [{c['score']:>5}] {c['host']} {c['user']} {c['process']}") print(f" {c['reasons']}") print(f" {c['cmdline'][:100]}") print(f"queue: {args.out / 'hunt_queue.csv'}")
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The four baselines (lines 46-56) are the hunt’s memory: fleet (process launch counts fleet-wide), host_seen (which processes each host has ever run in the window — the never-seen check), pair_count (process×parent pairs, the relationship baseline), and rare_threshold (the 5th percentile of fleet launch counts — rare means measurably rare, not vibes). The candidate loop (lines 61-103) stacks four independent signals per event: never-seen-on-host (+3.0 — the classic first-execution tell), LOLBin with suspicious arguments (+4.0 — the argument scan checks the seven script/download patterns that separate admin tooling from abuse; certutil -urlcache http is the canonical hit), script artifacts (+1.0), fleet rarity (+2.0), and rare parent pairs (+2.5 — Office spawning rundll32 is the T1566.001 follow-through this module hunts).
Deliberate design choices worth noticing: process identity normalizes to basename (lines 66-68) — path masquerading (C:\Windows\Temp\cmd.exe) is itself a finding the cmdline preserves; the LOLBin argument check is case-folded and substring-based (cheap, deterministic, and the reason the SUSPICIOUS_ARGS list stays curated — seven patterns, maintained like detection content, not a regex swamp); and the output carries the evidence line (cmdline truncated to 120 chars) with every candidate — the analyst verifies from the queue, not from a re-query. The queue caps at --top (default 25) because a hunt queue of 900 entries is a triage backlog wearing a hunting costume; if your true positive rate lives at rank 200, raise the cap consciously.
How It Reaches The Target
Section titled “How It Reaches The Target”The hunt cycle runs weekly with a theme (BT10’s PIR sets the quarter: “credential access” this month). Monday: the hunter pulls the process-launch export (30-day window from the BT3 pipeline — Sysmon EID 1 where deployed, 4688 elsewhere) and runs rareproc. Tuesday: the queue gets triaged — most entries are explainable (a new build tool on three dev hosts, the rare-but-legit diskshadow from the backup reconfiguration); the explainable ones annotate the baseline (a note in the hunt journal, so next week’s hunt doesn’t re-chase them). Wednesday: three entries resist explanation — one is certutil -urlcache http on a host whose owner is on leave; BT1 opens a case, BT5 captures evidence before the host changes.
The conversion moment: the hunt finds mshta launched from a user-writable temp path via a rare parent pair — real finding, contained (BT4), and the pattern becomes a BT9 rule (parent pair + path heuristic) that permanently watches for what the hunt caught once. The hunt journal records outcome detection, links the rule’s PR, and the hunt-to-detection counter ticks — the metric that keeps the program honest. The gap declaration happens too: the hunter’s T1003.001 hypothesis (LSASS access patterns) stalls because Sysmon EID 10 isn’t deployed on the server fleet — the journal records it as telemetry-gap, BT3’s backlog gets the request with the hunt’s business case attached, and the hunt re-opens the day the telemetry lands. What the adversary experiences: their next staging attempt meets a rule born from a hunt, on telemetry a gap-hunt fought to deploy — the hunting program’s compounding return.
Exploitation Chain
Section titled “Exploitation Chain”- Theme — quarter’s hunt themes set by BT10 PIRs and BT9’s coverage gaps
- Hypothesis — falsifiable statement per hunt: technique, source, observable, baseline logic
- Execute — rareproc / stack counts / peer compare over the BT3 pipeline export
- Triage — queue verified; explainable entries annotate the baseline journal
- Escalate — unexplained candidates → BT1 case; BT5 evidence before host state changes
- Convert — real patterns become BT9 rules; hunt journal outcome: detection
- Gap-declare — hunts that can’t run generate BT3 telemetry requests with business case
- Compound — next week’s hunt runs on the enriched baseline and the new rule watches forever
Building Your Own Tool
Section titled “Building Your Own Tool”Hunts that aren’t journaled didn’t happen — six months later nobody remembers what was checked, what was found, or why the baseline excludes that weird build server. huntlog is the program’s memory: hunts recorded as first-class records (hypothesis, technique, sources, queries, outcome, conversion link), with the conversion metrics that keep the program funded.
Design decisions
Section titled “Design decisions”Outcomes are an enum (finding|detection|gap|no-result), not free text — the conversion metrics depend on the taxonomy holding. Every hunt names its ATT&CK technique(s) — the join to BT9’s coverage matrix and DeTT&CT-style data mapping runs on this field. Hypothesis must be present and falsifiable-checked (the validator rejects “look for weird stuff”). The journal is append-only with supersede semantics (re-opening a hunt = new record linking the old), matching BT5’s evidence philosophy. Metrics read from the taxonomy: conversion rate, gap rate (BT3’s business-case pipeline), and per-source usage (which telemetry actually earns its ingestion cost).
Full source
Section titled “Full source”#!/usr/bin/env python3"""huntlog — hunt journal + conversion metrics for the hunting program.
Commands: add --hypothesis "..." --technique T1053.005 --sources sysmon,winlog --query-file q.kql [--outcome finding|detection|gap|no-result] close --id H-0042 --outcome detection --link PR-118 metrics [--quarter 2026Q4]Journal: append-only JSONL (hunt_journal.jsonl)."""import argparseimport jsonimport sysfrom collections import Counterfrom datetime import datetimefrom pathlib import Path
LOG = Path("hunt_journal.jsonl")OUTCOMES = ("finding", "detection", "gap", "no-result")T_RE = None # compiled lazily
def now(): return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
def next_id(entries): return f"H-{len(entries) + 1:04d}"
def load(): if not LOG.exists(): return [] out = [] with LOG.open(encoding="utf-8") as fh: for n, line in enumerate(fh, 1): try: out.append(json.loads(line)) except json.JSONDecodeError: sys.exit(f"fail-loud: journal line {n} corrupt") return out
def append(rec): with LOG.open("a", encoding="utf-8") as fh: fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
def cmd_add(a): import re global T_RE T_RE = re.compile(r"^T\d{4}(\.\d{3})?$") techs = [t.strip().upper() for t in a.technique.split(",") if t.strip()] for t in techs: if not T_RE.match(t): sys.exit(f"fail-loud: technique id invalid: {t!r} (need T#### or T####.###)") hyp = a.hypothesis.strip() if len(hyp) < 40 or not any(w in hyp.lower() for w in ("would", "should", "appear", "expect", "visible", "if")): sys.exit("fail-loud: hypothesis must be a falsifiable statement — " "phrase the prediction (e.g. 'X would appear as Y')") if not a.sources: sys.exit("fail-loud: hunts must name their telemetry sources") rec = {"id": next_id(load()), "ts": now(), "status": "open", "hypothesis": hyp, "techniques": techs, "sources": [s.strip() for s in a.sources.split(",")], "query_file": a.query_file, "outcome": None, "link": None} if a.query_file and not Path(a.query_file).exists(): sys.exit(f"fail-loud: query file not found: {a.query_file}") append(rec) print(f"{rec['id']} opened: {hyp[:80]}")
def cmd_close(a): entries = load() if a.outcome not in OUTCOMES: sys.exit(f"fail-loud: outcome must be one of {OUTCOMES}") for rec in reversed(entries): if rec["id"] == a.id: if rec["status"] == "closed": sys.exit(f"fail-loud: {a.id} already closed — supersede, don't edit") rec["status"] = "closed" rec["outcome"] = a.outcome rec["link"] = a.link rec["closed"] = now() # rewrite journal with the closure (append-only philosophy: # the closed record is the last statement of this hunt) with LOG.open("w", encoding="utf-8") as fh: for e in entries: fh.write(json.dumps(e, ensure_ascii=False) + "\n") print(f"{a.id} closed as {a.outcome}" f"{' -> ' + a.link if a.link else ''}") return sys.exit(f"fail-loud: hunt {a.id} not found")
def cmd_metrics(a): entries = load() closed = [e for e in entries if e["status"] == "closed"] if not closed: sys.exit("fail-loud: no closed hunts — metrics need outcomes") outcomes = Counter(e["outcome"] for e in closed) total = len(closed) conv = (outcomes["detection"] + outcomes["finding"]) / total print(f"hunts total={len(entries)} closed={total}") for o in OUTCOMES: print(f" {o:<11}: {outcomes.get(o, 0)}") print(f"conversion rate (finding+detection): {conv:.0%}") src = Counter(s for e in entries for s in e["sources"]) print("source usage:") for s, n in src.most_common(): print(f" {s:<14} {n} hunts") open_hunts = [e for e in entries if e["status"] == "open"] if open_hunts: print(f"OPEN HUNTS ({len(open_hunts)}): stale hunts rot — close or supersede") for e in open_hunts: print(f" {e['id']}: {e['hypothesis'][:60]}")
def main(): ap = argparse.ArgumentParser(description="hunt journal + metrics") sub = ap.add_subparsers(dest="cmd", required=True)
p1 = sub.add_parser("add") p1.add_argument("--hypothesis", required=True) p1.add_argument("--technique", required=True) p1.add_argument("--sources", required=True) p1.add_argument("--query-file") p1.set_defaults(func=cmd_add)
p2 = sub.add_parser("close") p2.add_argument("--id", required=True) p2.add_argument("--outcome", required=True) p2.add_argument("--link") p2.set_defaults(func=cmd_close)
p3 = sub.add_parser("metrics") p3.add_argument("--quarter") p3.set_defaults(func=cmd_metrics)
args = ap.parse_args() args.func(args)
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The falsifiability gate (lines 40-48) is the module’s doctrine in code: hypotheses must contain prediction language (“would appear as”, “should”, “if”) and exceed a length floor — “look for weird stuff” is refused at the door, which is stroll-hunting’s airport security. Technique IDs validate against the ATT&CK shape (lines 41-44: T#### or T####.###) so the join to coverage matrices never inherits typos. The query-file existence check (line 50) makes the hunt reproducible — the query is an artifact, not a memory.
Closure semantics (lines 54-70) encode the append-only philosophy with one pragmatic exception: closing rewrites the journal file to attach the outcome to the original record — but only forward (already-closed hunts refuse re-editing, line 60; supersede by opening a linked successor). The metrics command (lines 72-90) computes the program’s economics: conversion rate (finding + detection) / closed) is the fundable number; source-usage distribution answers the BT3 cost conversation (“DeTT&CT says we mapped EID 22 hunting five times this year — that source earns its keep”); and the open-hunts listing (lines 88-92) is the rot alarm — hunts are perishable, and stale ones get closed as no-result or superseded.
Compilation and test procedure
Section titled “Compilation and test procedure”open a hunt with a falsifiable hypothesis
Section titled “open a hunt with a falsifiable hypothesis”$ python3 huntlog.py add
—hypothesis “T1053.005 scheduled tasks created outside change windows would appear as 4698 events from non-admin contexts”
—technique T1053.005 —sources winlog,sysmon —query-file q_sched.kql
H-0001 opened: T1053.005 scheduled tasks created outside change windows woul…
a stroll-hunt refuses to open
Section titled “a stroll-hunt refuses to open”$ python3 huntlog.py add —hypothesis “look for weird stuff”
—technique T1003 —sources sysmon
fail-loud: hypothesis must be a falsifiable statement — phrase the prediction
close with conversion; metrics read the taxonomy
Section titled “close with conversion; metrics read the taxonomy”$ python3 huntlog.py close —id H-0001 —outcome detection —link PR-118 H-0001 closed as detection -> PR-118
$ python3 huntlog.py metrics hunts total=3 closed=2 finding : 0 detection : 1 gap : 1 no-result : 0 conversion rate (finding+detection): 50% source usage: winlog 3 hunts sysmon 2 hunts OPEN HUNTS (1): stale hunts rot — close or supersede
Detection considerations
Section titled “Detection considerations”The journal is program telemetry: conversion rate below ~30% across a quarter means hypotheses are drifting from the environment (re-anchor to BT10 PIRs and BT4 RCAs); gap-rate above ~40% is a BT3 funding case, not a hunting failure. The baseline journal (rareproc’s annotations) needs the same stewardship — explainable entries must persist or every weekly hunt re-chases the same build server. Watch for the inverse-Goodhart trap: conversion rate itself becomes a target, and hunters stop declaring gaps (gaps don’t convert) — the metrics command prints gap rate beside conversion rate precisely so the pair is always read together, the same discipline BT20’s counterpart doctrine enforces everywhere else.
Improvements to add
Section titled “Improvements to add”- Link hunts to BT9 rule PRs automatically and compute per-rule “born from hunt” provenance in the coverage matrix.
- Add peer-group definitions (host role tags) so
rareprocand future tools share one grouping truth. - Emit a DeTT&CT-style data-source coverage view from the sources field (which techniques are huntable with current telemetry).
- Time-box tracker: hunts older than N weeks auto-flag with a close-or-supersede decision request.
- Import BT4 RCA “surprises” as candidate hypotheses — the incident-derived hunt queue.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Never-seen process on host | rareproc | score >= 3 | First-execution tell |
| LOLBin suspicious args | rareproc | score >= 4 | Argument baseline |
| Rare parent pairs | rareproc | pair count <= 2 | Office→script chains |
| LSASS access patterns | Sysmon | EID 10 | Gap-hunt driver |
| Script block at odd hours | Windows Security | 4104 | LOLBin adjacents |
| New service installs | Windows Security | 7045 | Staging classic |
| Hunt conversion rate | huntlog metrics | quarterly | Program economics |
| Telemetry gap rate | huntlog metrics | quarterly | BT3 funding case |
| Baseline annotation count | hunt journal | per week | Knowledge compound |
| Data-source coverage map | DeTT&CT join | quarterly | Huntable techniques |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries hunt the hunters — or rather, exploit what hunts reliably miss. Baseline poisoning: low-grade noise (benign LOLBin usage by admins, scheduled tasks that look like staging) teaches the baseline that abuse-shaped behavior is normal; slow pollution over months beats the weekly window. Living in the crowd: attackers run staging during change windows and deploy campaigns (upgrade storms, backup jobs) when your baseline expects weirdness — hunting’s calendar discipline (hunting inside known-noisy windows occasionally) counters it. Peer-blending: malware that mimics the peer group’s profile (a compromised SQL server behaving exactly like the noisiest SQL server) defeats peer comparison; only cross-signal joins (network + process + auth) catch the blend. Tooling awareness: public hunt queries (GitHub is full of them) mean adversaries test their tradecraft against the community’s queries — your custom procedures (HM4) are the ones they can’t pre-test, which is exactly why the maturity model pays. Fragmented timelines: infrastructure attacks that never touch EDR (network devices, hypervisors, the backup server’s management plane) sit outside most hunt tooling — the gap declaration covers this honestly: can’t hunt there yet. The patient zero problem: first-execution baselines can’t fire on the host’s first-ever compromise if the attacker arrives before the baseline window opens — rolling windows and multi-source corroboration are the mitigation, not a cure.
⚠️ What caught me: my first real hunt-as-a-program cycle I ran
rareprocweekly like clockwork, closed 12 hunts, converted 4 rules, and felt like the HM4 hunter the maturity model describes. Then a tabletop reviewer asked to see the hunt journal and found six weeks where the same three build servers produced the same never-seennode.exeentries — re-chased every week because my annotations lived in a personal notebook, not the journal. The program’s memory was my memory, and it had a bus factor of one. The fix taught me the real hierarchy: hypothesis first, journal second, tooling third — rareproc without huntlog is a screen, not a program, and the conversion metric I was so proud of was quietly lying because the no-result hunts that taught me the most were the ones I never recorded.
Lab Practice
Section titled “Lab Practice”Run a four-week hunting cycle against the lab range you’ve built across the Blue Team modules. Setup: deploy Sysmon with a decent config on two Windows hosts + one Linux auditd; ship to the BT3 pipeline; generate two weeks of normal activity (logins, builds, backups). Week 1 — baseline: run rareproc on the export; annotate every candidate in huntlog (open hunts per hypothesis, close as no-result with notes). Week 2 — inject: have a colleague (or script) stage a T1053.005 scheduled task + a certutil -urlcache download outside business hours. Week 3 — hunt: re-run; confirm the inject surfaces in the queue; escalate via BT1’s process and capture evidence BT5-style. Week 4 — convert: write the BT9 detection for the pattern you caught, link it in the hunt journal, and compute your program’s first real conversion rate. Gap-declare: pick one hypothesis you cannot test (e.g., LSASS access without Sysmon EID 10) and write the BT3 telemetry request with a business case. Debrief: which output — finding, rule, or gap request — was most valuable to the lab program, and what does that say about where your real program should spend its next quarter?
Interview Questions
Section titled “Interview Questions”- Technical: Walk the three hunt outcomes and why telemetry-gap is a success — what does the gap declaration produce, and who consumes it?
- Technical: Explain
rareproc’s four baselines and why the LOLBin argument check scores higher than the name check. - Scenario: Your hunt queue’s #1 candidate is
certutil -urlcache httpon a database server whose admin is on vacation. Walk the decision: what do you check in the next 30 minutes before opening a case? - Scenario: Six months of hunting shows a 15% conversion rate. Is the program failing? Walk the diagnosis — what do you look at before touching headcount?
- Conceptual: Why does the Hunting Maturity Model treat documented procedure (HM3) as a bigger milestone than more tooling — and what breaks at an HM2 program whose one expert leaves?
- Deep-dive: Design the hunting program for a 5,000-host estate: hypothesis pipeline, telemetry sources with DeTT&CT-style mapping, the weekly cadence, conversion metrics, and the BT3 gap-request workflow.
- Practical: You have one hunter, 40 hours a month, and a BT10 picture pointing at credential-access techniques. Build the month’s hunt plan — and say what you deliberately skip.
Related Modules
Section titled “Related Modules”BT1 (SOC Operations & Alert Triage) · BT3 (SIEM Engineering & Log Pipelines) · BT9 (Detection Engineering & Sigma) · BT10 (Threat Intelligence Operations) · T9 (Atomic Testing & Automation)