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.

DetectorsHunts
OutputFindings (alerts)Rows (tabular candidates)
PrecisionHighLow to medium
RecallLowerHigh
When to runEvery scanOn-demand, during investigation
AudienceSOC / on-callAnalyst / hunter
Where they livedigger/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

IDSeverity hintWhat it looks forMITRE
shai-hulud-packagescriticalnpm packages on the Shai-Hulud worm's compromised-version listT1195.002
browser-spawns-shellhighShell processes parented by a browser processT1059
encoded-powershellhighpowershell.exe with -EncodedCommand + long base64T1059.001
curl-pipe-bashhighcurl/wget/Invoke-WebRequest piped to bash/sh/iexT1105
dynamic-linker-hijackhighLD_PRELOAD / DYLD_INSERT_LIBRARIES set in environmentT1574.006
ssh-key-forced-commandhighauthorized_keys entries with command=T1098.004
interpreter-in-tempmediumpython/node/ruby/perl running from /tmp, %TEMP%, etc.T1059
shell-init-hookmediumBASH_ENV / ENV / PROMPT_COMMAND setT1546.004
process-without-exe-pathmediumLive processes with no exe on disk (memfd, unlinked, …)T1055
tor-exit-connectionmediumEstablished connection to a Tor bulk-exit IPT1090.003
browser-extension-sweeping-permslowExtensions with <all_urls>, debugger, nativeMessaging, …T1176
uncommon-listenerlowLISTEN sockets outside the common service-port setT1571
persistence-in-user-homelowPersistence entries pointing at /Users/<name>/ or /home/T1547
recent-executable-in-droplowRecently-modified executables in /tmp, Downloads, %TEMP%T1564.001
high-entropy-domainlowBrowser-history domains whose left-most label has high entropyT1568.002
interpreter-with-external-connlowpython/node/ruby with an open established connection to a public IPT1059
large-authorized-keyslowauthorized_keys files with >5 active keysT1098.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