Threat hunting Exploratory tabular queries over the evidence store. Different from detectors — they prioritize recall over precision and return rows, not alerts.
Hunts vs detectors
The detector stack is precision-first: every emitted Finding is meant to be actionable. Hunts are the opposite — recall-first, false-positives are fine, and the output is a table the analyst eyeballs to find candidates worth investigating further.
| Detectors | Hunts | |
|---|---|---|
| Output | Findings (alerts) | Rows (tabular candidates) |
| Precision | High | Low to medium |
| Recall | Lower | High |
| When to run | Every scan | On-demand, during investigation |
| Audience | SOC / on-call | Analyst / hunter |
| Where they live | digger/detectors/ | digger/hunts/library.py |
Running hunts
digger hunt list # show every available hunt
digger hunt list -v # … with descriptions
digger hunt run --case-dir ./case-1 # run all hunts, dump results
digger hunt run --case-dir ./case-1 --hunt browser-spawns-shell,curl-pipe-bash
digger hunt run --case-dir ./case-1 --severity high
digger hunt run --case-dir ./case-1 --tag command-and-control
# Render an HTML report (collapsible per-hunt cards with full row tables)
digger hunt run --case-dir ./case-1 --out hunts.html
digger hunt run --case-dir ./case-1 --format md --out hunts.md
digger hunt run --case-dir ./case-1 --format json --out hunts.json
The CLI summary sorts non-empty hunts by severity (critical → info) and
then by row count. Empty hunts are hidden unless you pass -v.
The bundled library
| ID | Severity hint | What it looks for | MITRE |
|---|---|---|---|
shai-hulud-packages | critical | npm packages on the Shai-Hulud worm's compromised-version list | T1195.002 |
browser-spawns-shell | high | Shell processes parented by a browser process | T1059 |
encoded-powershell | high | powershell.exe with -EncodedCommand + long base64 | T1059.001 |
curl-pipe-bash | high | curl/wget/Invoke-WebRequest piped to bash/sh/iex | T1105 |
dynamic-linker-hijack | high | LD_PRELOAD / DYLD_INSERT_LIBRARIES set in environment | T1574.006 |
ssh-key-forced-command | high | authorized_keys entries with command= | T1098.004 |
interpreter-in-temp | medium | python/node/ruby/perl running from /tmp, %TEMP%, etc. | T1059 |
shell-init-hook | medium | BASH_ENV / ENV / PROMPT_COMMAND set | T1546.004 |
process-without-exe-path | medium | Live processes with no exe on disk (memfd, unlinked, …) | T1055 |
tor-exit-connection | medium | Established connection to a Tor bulk-exit IP | T1090.003 |
browser-extension-sweeping-perms | low | Extensions with <all_urls>, debugger, nativeMessaging, … | T1176 |
uncommon-listener | low | LISTEN sockets outside the common service-port set | T1571 |
persistence-in-user-home | low | Persistence entries pointing at /Users/<name>/ or /home/ | T1547 |
recent-executable-in-drop | low | Recently-modified executables in /tmp, Downloads, %TEMP% | T1564.001 |
high-entropy-domain | low | Browser-history domains whose left-most label has high entropy | T1568.002 |
interpreter-with-external-conn | low | python/node/ruby with an open established connection to a public IP | T1059 |
large-authorized-keys | low | authorized_keys files with >5 active keys | T1098.004 |
Writing your own
Hunts are plain Python — register a function and you're done.
from digger.core.evidence import EvidenceStore
from digger.hunts.base import Hunt, register
def hunt_my_thing(store: EvidenceStore):
for art in store.iter_artifacts(collector="processes"):
d = art["data"]
if (d.get("username") == "root"
and any(d.get("exe", "").startswith(p) for p in ("/tmp/", "/var/tmp/"))):
yield {
"pid": d.get("pid"),
"name": d.get("name"),
"exe": d.get("exe"),
"cmdline": " ".join(d.get("cmdline") or [])[:200],
"_artifact": art["artifact_uuid"],
}
register(Hunt(
id="root-from-tmp",
title="root-owned processes whose exe lives in /tmp",
description="Almost never legitimate; great signal for malicious "
"binaries dropped by a privileged exploit.",
severity_hint="high",
mitre="T1059",
tags=["execution", "privilege-escalation"],
columns=["pid", "name", "exe", "cmdline"],
fn=hunt_my_thing,
))
Drop the file under digger/hunts/ (or import the module
elsewhere so it runs at registration time). The next
digger hunt list will include it.
Sample output
For a live example rendered against a synthetic case, open sample-hunts.html (generated from this Mac's case data — only a couple of hunts return rows on a clean host).
When to reach for hunts
- After a
scanturns up something interesting and you want to look for the rest of the campaign on the same host. - During a baseline survey of a new host — run the whole library and eyeball anything non-empty.
- While responding to an alert from another tool — use hunts to find adjacent IOC instances quickly.
- As a periodic "is anything looking suspicious?" sweep:
digger hunt run --case-dir … --severity mediumin a cron job.