Building Effective Threat Hunting Queries

Oct 20, 2025 • 4 min read • Intermediate

  • Threat Hunting
  • SIEM Queries

Automated detections cover the behavior somebody already wrote a rule for. Hunting queries are for everything else, and the ones that work start from a specific idea about adversary behavior rather than a broad sweep across a data source.

Start from a hypothesis

“Check for PowerShell activity” is a data pull rather than a hunt, since it produces volume without defining what would count as a finding. “Threat actors may use encoded PowerShell commands to download payloads” gives you a specific TTP to test, a set of fields worth aggregating on, and a clear answer when the results come back empty.

Write for anomalies, not matches

The query below hunts encoded PowerShell and download cradles in Sysmon process creation events, then ranks what comes back by how rare it is.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
index=endpoint EventCode=1
(Image="*\\powershell.exe" OR Image="*\\pwsh.exe" OR OriginalFileName="PowerShell.EXE")
| where match(CommandLine, "(?i)[-/]e[a-z]*\s+[A-Za-z0-9+/=]{30,}")
     OR match(CommandLine, "(?i)downloadstring|downloadfile|downloaddata|invoke-webrequest|invoke-restmethod|start-bitstransfer|net\.webclient")
| eval shape=CommandLine
| rex mode=sed field=shape "s/https?:\/\/[^\s\"']+/<URL>/g s/[A-Za-z0-9+\/=]{30,}/<B64>/g"
| stats count as executions, dc(Computer) as hosts, values(User) as users,
        values(ParentImage) as parents, min(_time) as first, max(_time) as last by shape
| where hosts <= 2
| convert ctime(first) ctime(last)
| sort hosts executions

Stacking on the raw command line does not work for this, because an encoded payload is unique to every execution and each row comes back with a count of one. The sed replacement strips the base64 blob and any URLs out first, which leaves a skeleton that does repeat, so the aggregation has something real to count. Ranking by distinct hosts rather than raw volume follows from the same idea, since a shape appearing ten thousand times across the fleet is your build automation and a shape appearing twice on one host is worth reading. Two hosts is a starting threshold and wants tuning to the size of the environment.

The parameter match is loose on purpose. PowerShell accepts any unambiguous prefix of -EncodedCommand, so -e, -ec, -en, and -enco all execute, and a literal *-enc* wildcard misses most of them. Requiring a long base64 string immediately after the flag is what keeps that loose match from flooding the results. OriginalFileName is in the search for the same reason, since it comes out of the PE version resource and a copy of powershell.exe renamed to something unremarkable still matches on it.

The rarity stacking above is a hunt technique, not something a single Sigma rule can express, but the selection criteria underneath it travel fine. This is the same encoded-command and download-cradle logic as a standing, backend-portable detection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
title: Encoded PowerShell Command with Download Cradle Indicators
id: 9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f
status: experimental
description: |
  Flags PowerShell process creation using an encoded-command flag alongside a
  long base64 argument, or invoking a known download cradle. High volume by
  itself; pair with the hunting query above to stack by host rarity before
  treating results as findings.
references:
  - https://elmerphillips.com/blog/building-effective-threat-hunting-queries/
author: Elmer Phillips
date: 2025-10-20
tags:
  - attack.execution
  - attack.t1059.001
  - attack.t1027
logsource:
  category: process_creation
  product: windows
detection:
  selection_img:
    Image|endswith:
      - '\powershell.exe'
      - '\pwsh.exe'
  selection_encoded:
    CommandLine|re: '(?i)[-/]e[a-z]*\s+[A-Za-z0-9+/=]{30,}'
  selection_cradle:
    CommandLine|contains:
      - 'DownloadString'
      - 'DownloadFile'
      - 'DownloadData'
      - 'Invoke-WebRequest'
      - 'Invoke-RestMethod'
      - 'Start-BitsTransfer'
      - 'Net.WebClient'
  condition: selection_img and (selection_encoded or selection_cradle)
falsepositives:
  - Legitimate automation and deployment scripts that download payloads over HTTP
  - Administrative scripts using short-form encoded-command flags for unrelated reasons
level: medium

Scope the window to the hypothesis

An incident investigation scopes to the hours around the suspicious activity. Proactive hunting works better sampled incrementally across recent data than run over a full retention period at once, since a query returning thousands of rows gets skimmed instead of reviewed, and the reason to hunt at all is that every result gets looked at.

Evaluating results

A result earns its place if it confirms or refutes the hypothesis, exposes a pattern you had not considered, or gives you a pivot point into other data. Anything else is noise, and noise usually means the scope is too broad or the hypothesis was not specific enough to begin with. Both are worth fixing before running the query again.

Document the hunt

Record the hypothesis, the queries, the findings, and the conclusion, including the hunts that turned up nothing. Knowing which patterns have already been checked and came back clean is most of what makes the next hunt faster, and it is the part that gets skipped first.

Elmer Phillips, Security Analyst