Skip to content

Runbook: Monthly Threat Hunt Scope

Purpose

Automated detection catches known-bad patterns. A threat hunt looks for the unknown — adversary activity that slipped past the agents, weak signals hidden in noise, and behaviors that only make sense when a human inspects them. The monthly threat hunt is a structured, hypothesis-driven exercise that forces us to look at our environment from an attacker's point of view.

Estimated time: 3–4 hours. Run on the second Wednesday of each month. File results in bedrock-docs/threat-hunting/hunt-<NNN>-<short-name>.md.


Scope — What a Monthly Hunt Covers

Each monthly hunt covers five categories. All five are run every month — rotation happens within each category (which servers, which accounts, which time window). The hunter picks one focused hypothesis per category for that month and documents findings even when nothing is found.

1. Lateral movement

Hypothesis themes: "An attacker with a valid credential is moving between hosts using legitimate tools."

  • Same user account authenticating to 3+ distinct hosts within 30 minutes
  • Use of administrative shares (C$, ADMIN$, IPC$) outside normal patching windows
  • Explicit credential logons (4648) across server tiers (workstation → server)
  • WMI, WinRM, PsExec, or SMB-based remote execution from non-admin workstations
  • Unexpected RDP chains (workstation → server → server)

2. Persistence

Hypothesis themes: "An attacker has installed a foothold that survives reboot."

  • New Windows services installed (7045) outside CM windows
  • New scheduled tasks (4698) — especially in \Microsoft\Windows\ hiding among system tasks
  • Registry Run keys added in user or machine hives
  • WMI event subscriptions (rarely legitimate)
  • Startup folder entries on any host
  • New AutoLogger / ETW sessions (evasion)

3. Unusual processes

Hypothesis themes: "A process is running that shouldn't be, or a legitimate process is being abused."

  • LOLBin usage: rundll32, regsvr32, mshta, wmic, certutil, bitsadmin
  • Scripts spawned from Office apps (Word, Excel, Outlook → powershell.exe or cmd.exe)
  • Encoded PowerShell commands (-enc, -encodedcommand, base64 strings)
  • Processes executing from user-writable paths (%TEMP%, %APPDATA%, C:\Users\Public)
  • Unsigned binaries running as SYSTEM or as local admin
  • Parent-child anomalies (e.g. services.exe spawning cmd.exe)

4. Exfiltration

Hypothesis themes: "Data is leaving the environment through an unusual channel or destination."

  • Outbound bytes from any host exceeding its 30-day baseline by 3x
  • New outbound destinations not seen in the last 30 days
  • DNS queries to known tunneling domains or high-entropy subdomains
  • Cloud storage uploads (Mega.nz, AnonFiles, file.io, Rclone, WinSCP user-agent)
  • Encrypted archive creation (.7z, .rar) followed by outbound transfer
  • Large file transfers outside business hours (especially 00:00–05:00)

5. New external connections

Hypothesis themes: "A host is talking to something on the internet it has never talked to before."

  • First-seen external IPs/domains per host in the last 30 days
  • Connections to high-risk ASNs or geographies (VPS providers, adversary-known ASNs)
  • Non-standard ports to external IPs (4444, 8080 from non-proxy hosts)
  • Direct-to-internet connections from hosts that normally egress via the proxy
  • DNS tunneling indicators (unusually long subdomains, high request rate)

Queries and Artifacts Per Category

The hunter can use either KQL (Log Analytics) or Velociraptor artifacts depending on what signal they need. Always record which query was used and the time window in the hunt report.

1. Lateral movement

KQL — user hitting multiple hosts in 30 min:

kql
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID in (4624, 4648)
| where LogonType in (3, 10)
| where AccountType == "User"
| summarize hosts = dcount(Computer), host_list = make_set(Computer, 20)
  by Account, bin(TimeGenerated, 30m)
| where hosts >= 3
| where Account !startswith "svc-" and Account !startswith "kobe-"
| project TimeGenerated, Account, hosts, host_list
| order by TimeGenerated desc

KQL — admin share access (5140):

kql
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID == 5140
| where ShareName endswith "C$" or ShareName endswith "ADMIN$"
| summarize count() by Account, Computer, ShareName, bin(TimeGenerated, 1h)
| where count_ > 0
| order by TimeGenerated desc

Velociraptor artifacts:

  • Windows.Forensics.Lnk — which files were recently accessed across hosts
  • Windows.EventLogs.RDPAuth — RDP session chains
  • Windows.EventLogs.ServiceCreation — PsExec leaves PSEXESVC traces

2. Persistence

KQL — new services (7045):

kql
Event
| where TimeGenerated > ago(30d)
| where EventLog == "System" and EventID == 7045
| extend ServiceName = extract(@"Service Name:\s+([^\s]+)", 1, RenderedDescription)
| extend ImagePath = extract(@"Service File Name:\s+([^\r\n]+)", 1, RenderedDescription)
| where ServiceName !in ("Windows Defender Advanced Threat Protection Service",
                          "CSAgent", "cyserver", "ArcticWolfAgent")
| project TimeGenerated, Computer, ServiceName, ImagePath
| order by TimeGenerated desc

KQL — new scheduled tasks (4698):

kql
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID == 4698
| extend TaskName = extract(@"<TaskName>([^<]+)</TaskName>", 1, EventData)
| where TaskName !startswith "\\Microsoft\\"
       or TaskName matches regex @"\\Microsoft\\Windows\\[^\\]+\\[A-Za-z0-9]{16,}"
| project TimeGenerated, Computer, Account, TaskName
| order by TimeGenerated desc

Velociraptor artifacts:

  • Windows.System.Services — current services snapshot
  • Windows.System.TaskScheduler — scheduled task inventory
  • Windows.Registry.Sysinternals — common persistence registry keys
  • Windows.Persistence.PermanentWMIEvents — WMI event subscription persistence

3. Unusual processes

KQL — LOLBin usage (requires 4688 + command line capture):

kql
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID == 4688
| extend Image = tostring(NewProcessName)
| where Image has_any ("rundll32.exe", "regsvr32.exe", "mshta.exe",
                        "wmic.exe", "certutil.exe", "bitsadmin.exe")
| extend CmdLine = tostring(CommandLine)
| where CmdLine !has "Windows Defender" and CmdLine !has "Microsoft\\Edge"
| project TimeGenerated, Computer, Account, Image, CmdLine
| order by TimeGenerated desc

KQL — Office spawning shells:

kql
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID == 4688
| extend Parent = tostring(ParentProcessName)
| where Parent has_any ("winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe")
| extend Child = tostring(NewProcessName)
| where Child has_any ("powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe",
                        "mshta.exe", "rundll32.exe")
| project TimeGenerated, Computer, Account, Parent, Child, CommandLine
| order by TimeGenerated desc

Velociraptor artifacts:

  • Windows.Detection.Amcache — execution evidence from AmCache
  • Windows.Detection.ForeignUsers — unusual user contexts
  • Generic.Detection.Yara.Process — Yara scan of running processes

4. Exfiltration

KQL — outbound byte volume per host (Palo Alto traffic via CEF):

kql
CommonSecurityLog
| where TimeGenerated > ago(30d)
| where DeviceVendor == "Palo Alto Networks"
| where DeviceAction == "allow"
| where DestinationIP !has_prefix "10." and DestinationIP !has_prefix "192.168."
       and DestinationIP !has_prefix "172.16."
| summarize outbound_gb = sum(SentBytes) / 1024.0 / 1024.0 / 1024.0
  by SourceIP, bin(TimeGenerated, 1d)
| where outbound_gb > 1.0
| order by outbound_gb desc

KQL — first-seen external destinations per host:

kql
let baseline = CommonSecurityLog
| where TimeGenerated between (ago(60d) .. ago(30d))
| where DeviceAction == "allow"
| summarize by SourceIP, DestinationIP;
CommonSecurityLog
| where TimeGenerated > ago(30d)
| where DeviceAction == "allow"
| where DestinationIP !has_prefix "10." and DestinationIP !has_prefix "192.168."
| join kind=leftanti baseline on SourceIP, DestinationIP
| summarize first_seen = min(TimeGenerated), connections = count()
  by SourceIP, DestinationIP
| where connections >= 3
| order by first_seen asc

Velociraptor artifacts:

  • Windows.Network.NetstatEnriched — current connections with process context
  • Generic.Forensic.LocalHashes.Glob — find staged archives in %TEMP%

5. New external connections

KQL — unusual ASN destinations:

kql
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where DeviceAction == "allow"
| where DestinationIP !startswith "10." and DestinationIP !startswith "192.168."
| summarize by SourceIP, DestinationIP, DestinationPort
| join kind=inner (externaldata(IP:string, ASN:int, Country:string)
    [@"https://cirius-threat-intel-prod.blob.core.windows.net/asn/asn.csv"]
    with (format="csv", ignoreFirstRecord=true)) on $left.DestinationIP == $right.IP
| where ASN in (dynamic([16509, 14061, 9009, 20473]))  // VPS/bulletproof ASNs — adjust list
| project SourceIP, DestinationIP, DestinationPort, ASN, Country

KQL — long DNS subdomain patterns (tunneling indicator):

kql
DnsEvents
| where TimeGenerated > ago(7d)
| extend subdomain_len = strlen(tostring(split(Name, ".")[0]))
| where subdomain_len > 40
| summarize count() by ClientIP, Name
| order by count_ desc

Velociraptor artifacts:

  • Windows.Network.ArpCache — unusual local network peers
  • Windows.Network.Netstat — current listeners and connections

How to Document Results

Every hunt produces a markdown report in bedrock-docs/threat-hunting/ named hunt-<NNN>-<short-name>.md where <NNN> is the sequential hunt number starting at 001. Use hunt-001-baseline.md as the template.

Each report must include:

  1. Hunt metadata: hunter, date, scope (PROD / DDE / AWS), time window, hunt number
  2. Hypotheses: one per category, stated as "We believe X may be happening because Y"
  3. Queries run: KQL snippets or Velociraptor artifact names with parameters
  4. Findings: for each hypothesis — confirmed, not confirmed, or inconclusive
  5. Incidents raised: any SecOps incidents created with their IDs
  6. New detections needed: patterns worth promoting from hunt to agent-based detection
  7. Known-good rules added: any new suppression rules proposed (Rory approves)
  8. Evidence location: S3 path where hunt result JSON was uploaded (see HUNT-011 / threat_hunt_evidence.py)

Sign-off: hunter commits the report, Rory reviews in PR, merged report is the authoritative record of the hunt.


Month-over-Month Tracking

Keep a running summary so we can see what's been covered and what hasn't:

  • Category rotation log — which hypothesis was tested per category per month
  • Detection gaps surfaced — hunts that found "we can't answer this with current logging" become backlog items
  • New agents proposed — any pattern seen 2+ times in hunts is a candidate for a detection agent (kill chain Phase 2 pipeline)

  • runbooks/monthly-security-review.md — monthly compliance/control review (different cadence, different purpose)
  • runbooks/incident-response.md — what to do when a hunt confirms a real incident
  • threat-hunting/hunt-001-baseline.md — template + first hunt record
  • compliance/hipaa-controls.md — SOC2 CC7.1 and HIPAA 164.312(a)(1) evidence mapping

Internal use only — Cirius Group