Threat Hunting: Hypotheses, Data & Repeatable Hunts

Defender-side threat hunting: hypotheses from ATT&CK and intel, endpoint, DNS, proxy, identity and cloud data, baselines, stacking, queries and hunts that become detections

Read the free first lesson below โ€” 12 lessons ยท CHF 9.90 one-time

12 lessons ยท knowledge checks ยท final exam ยท verifiable certificate

Unlock all 12 lessons ยท CHF 9.90 one-time Open in the course reader

Free lesson 1: Threat Hunting Foundations: The Hunt Loop and a Complete Worked Hunt

22 min read ยท lesson 1 of 12 ยท free, no sign-up

Network graph of connected points with an orange crosshair locking onto a cluster of red anomalies
Free lesson ยท Threat hunting

Your SIEM is quiet and the EDR console is green. Does that mean nobody is inside your network โ€“ or only that nobody has done anything your rules were written to catch? Threat hunting answers that question on purpose. In this lesson you learn what hunting is, the hunt loop professionals use and how to write a testable hypothesis. Then you follow one complete hunt from the first idea to a confirmed finding, a new detection and a written record โ€“ and a second, shorter hunt on identity data.

  • 12lessons, from first hypothesis to a team hunt programme
  • 6stages in the hunt loop you learn here
  • 2complete hunts worked through in this lesson
  • ~20 minreading time for this lesson
The six-stage hunt loop Six boxes arranged in a loop: hypothesis, data check, query and stack, investigate outliers, outcome, and improve. Arrows connect each stage to the next, and the improve stage feeds back into the next hypothesis. A side branch from investigate leads to incident response when a real threat is found. 1 Hypothesis behaviour ยท scope ยท data 2 Data check does the evidence exist? 3 Query & stack count ยท rank ยท filter real threat โ†’ IR 4 Investigate explain every outlier 5 Outcome finding ยท gap ยท baseline 6 Improve detection ยท logging ยท record The loop restarts with a better hypothesis and better data
Figure 1. The hunt loop used throughout this course. Stages 1โ€“3 turn an idea into evidence, stage 4 is where analysts spend most of their time, and stages 5โ€“6 make sure every hunt leaves the organisation better defended โ€“ even when nothing malicious is found. A confirmed threat leaves the loop and goes straight to incident response.

What threat hunting is โ€“ and what it is not

Threat hunting is the proactive, human-led search for adversary activity that your existing detections have not reported. Proactive: nothing has to fire first โ€“ you start from a question, not an alert. Human-led: an analyst forms the question, decides what evidence answers it and judges the result. Tools count; people reason.

Hunting rests on one uncomfortable assumption: assume breach. You do not assume you are compromised, but you accept that a capable adversary could be present without tripping a single rule. Rules cover behaviour someone has already imagined; attackers using valid accounts, built-in admin tools or ordinary web traffic can stay below every threshold. Hunting looks into exactly that space.

ActivityStarts withQuestion it answersTypical output
Alert triageA rule firedIs this alert real, and how bad is it?Verdict, escalation or closure
Incident responseA confirmed incidentWhat happened and how do we recover?Containment, report
IOC sweepA list of hashes, IPs or domainsDo any known-bad indicators appear in our data?Matches or no matches
Threat huntingA hypothesis about attacker behaviourIf this were happening here, where would we see it โ€“ and do we?Findings, data gaps, baselines, new detections

An IOC sweep is useful but shallow: hashes and IP addresses are cheap for an adversary to change, while behaviour โ€“ how they persist, move and talk to their infrastructure โ€“ is not. David Bianco's Pyramid of Pain captures this: the higher you detect (from hashes up to tactics, techniques and procedures), the more evasion costs the adversary. Good hunts aim high.

A hunt that finds nothing can still succeed

Beginners measure a hunt by whether it found an attacker; professionals by what it changed. A well-designed hunt ends in one or more of four outcomes: a finding handed to incident response; a data gap (the evidence you needed was not collected or not searchable); a baseline (you now know what normal looks like for this behaviour); or a new or improved detection so that next time a rule does the work. Only the first is rare; the other three come out of nearly every hunt and make a team steadily harder to surprise.

The hunt loop: six stages from idea to improvement

Published models such as the Sqrrl hunting loop, the TaHiTI methodology from the Dutch financial sector and the PEAK framework (Prepare, Execute, Act with Knowledge) share the same logic. This course uses a six-stage version because each stage maps to a concrete piece of work you can check off:

  • 1 Hypothesisa specific behaviour, scope and data source
  • 2 Data checkis the evidence collected, parsed and complete?
  • 3 Query & stackcount, rank and filter to a short list
  • 4 Investigateexplain every outlier or escalate it
  • 5 Outcomefinding, gap, baseline โ€“ written down
  • 6 Improvedetections, logging, next hypothesis

Two rules govern the loop. First, stage 2 is not optional: hunts often fail because the data did not contain what the hunter assumed โ€“ a source that stopped forwarding, an empty field, retention shorter than the hunt window. Second, a confirmed threat stops the hunt: likely live attacker activity becomes an incident with its own process. Hand it over and finish the hunt record later.

Three ways a hunt starts

Hunts are hypothesis-driven (an ATT&CK technique such as scheduled-task persistence, T1053.005), intelligence-driven (a report on a group that targets your sector) or analytics-driven (a dashboard shows one workstation contacting a domain no other host uses). In practice they blend: intelligence suggests a behaviour, ATT&CK names it, analytics show where to look first. Lessons 2 and 3 turn each trigger into a repeatable planning step.

Writing a hypothesis you can actually test

"Let's look for bad stuff in the proxy logs" is not a hypothesis: no behaviour, no scope, no end. A testable hypothesis has four parts and fits in one sentence:

"Adversaries may be using [behaviour, ideally an ATT&CK technique] on [scope: systems, users, time range], which would be visible as [observable pattern] in [data source]."

The behaviour keeps you focused and the scope keeps the hunt finite. The observable defines a positive result before you see data, so you cannot talk yourself into "something odd". The data source tells you where to run stage 2 โ€“ and if you do not collect it, the hunt has already produced its first outcome: a logging gap.

Vague
  • "Check for malware calling home."
  • "Look at weird logons."
Testable
  • "Adversaries may use web-protocol C2 (T1071.001) from Windows workstations, visible as highly regular requests to rarely seen domains in 30 days of proxy logs."
  • "Adversaries may password-spray our cloud identity provider (T1110.003), visible as one source failing for many distinct accounts in sign-in logs."

Under the hood: what a hunter actually computes

Most hunting is not exotic: it is counting things in a smart order and measuring how unusual a value is. Here is the raw material and the two calculations behind the worked hunt.

One normalised proxy event

A web proxy records each outbound request. After your log pipeline parses and normalises it, a single event might look like this (synthetic example):

{
  "time":        "2026-06-29T08:15:02Z",
  "src_host":    "WS-FIN-044",
  "src_ip":      "10.20.14.44",
  "user":        "m.keller",
  "method":      "POST",
  "dest_domain": "cdn-sync.example.net",
  "dest_port":   443,
  "url_path":    "/api/v2/check",
  "status":      200,
  "bytes_out":   412,
  "bytes_in":    96,
  "user_agent":  "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
  "category":    "uncategorized"
}

Every field is a hunting dimension. dest_domain lets you measure how many hosts ever talk to a destination (its prevalence); time gives rhythm; bytes_out versus bytes_in hints at uploading versus checking in. For HTTPS, an explicit proxy logs the host name from the CONNECT request, but paths and user agents exist only with TLS inspection โ€“ a stage-2 question, because without it url_path and user_agent may be empty for most events.

Calculation 1: stacking by prevalence

Stacking (also called least frequency of occurrence) means grouping events by a value and counting. Group 30 days of proxy events by dest_domain, count requests and count distinct hosts. Domains used by hundreds of workstations are the company's normal internet. Domains used by one or two hosts and contacted hundreds of times are the interesting long tail: something on that machine wants to talk to that destination a lot, and nobody else does.

Calculation 2: timing regularity

Software on a timer produces near-constant intervals; people clicking links do not. A simple measure is the coefficient of variation (CV) of the gaps between requests: the standard deviation divided by the mean. A CV near 0 means a metronome; a CV above 1 means bursts and pauses. With five gaps of 298, 305, 291, 310 and 296 seconds, the mean is 300 s, the sample standard deviation is about 7.5 s and the CV is 0.025. With five gaps from a person browsing โ€“ 12, 340, 45, 1800 and 95 seconds โ€“ the mean is about 458 s, the standard deviation about 761 s and the CV about 1.66.

Request timing of a timer-driven client compared with a person browsing Two horizontal timelines over 60 minutes. The upper timeline shows requests from a timer-driven client as evenly spaced marks every five minutes, labelled CV about 0.03. The lower timeline shows requests from a person browsing as irregular clusters with long pauses, labelled CV above 1. Timer-driven client: one request every ~300 s (CV โ‰ˆ 0.03) Person browsing: bursts and pauses (CV > 1) 0 min15304560 min
Figure 2. Why timing matters. Both lines contain 13 requests in one hour, so a simple count cannot tell them apart. The spacing can. Synthetic data drawn for illustration.

Neither measure proves anything alone โ€“ update agents, monitoring consoles and chat clients all poll on timers. That is why you stack first: timing is computed only for destinations that are already rare, so the legitimate pollers left are few enough to explain one by one.

Worked hunt: timer-driven web traffic to rare destinations

The organisation is fictional: a logistics company with about 1,200 Windows workstations, a web proxy for all outbound web traffic and EDR everywhere. All data is synthetic. Queries use a generic pipe-based pseudo-syntax (not tied to any product) and SQL over a table of normalised events.

  1. Write the hypothesis and set a timebox. "Adversaries with a foothold on Windows workstations may use web-protocol command and control (ATT&CK T1071.001), visible as low-volume, highly regular requests from one or a few hosts to destinations that the rest of the company never contacts, in the last 30 days of proxy logs." Scope: all workstations; servers excluded for this round. The hunt starts on Wednesday 1 July 2026, so "last 30 days" means June. Timebox: two analyst days. Stop condition: every candidate is explained or escalated.
  2. Check the data before trusting it. Count how many distinct workstations appear in the proxy logs per day. If much of the fleet is missing, "rare" only means "unseen".
    # pseudo-query: proxy coverage per day
    from proxy
    | where time > ago(30d) and src_host startswith "WS-"
    | stats hosts = dcount(src_host) by day = bin(time, 1d)
    | sort day asc

    Example output (synthetic):

    day          hosts
    2026-06-01    1094
    2026-06-02    1102
    ...
    2026-06-06      38    # Saturday: only always-on machines
    ...
    2026-06-29    1098
    2026-06-30    1101

    About 1,100 of 1,200 workstations report on a working day. Leave explains most of the gap, but 41 warehouse workstations never appear: their VLAN bypasses the proxy. Outcome number one โ€“ a data gap โ€“ is logged before the first hunting query.

  3. Stack destinations by prevalence. Group 30 days of requests by destination, count requests and distinct hosts, and keep the rare-but-busy ones.
    # generic pipe syntax - adapt names to your SIEM
    from proxy
    | where time > ago(30d) and src_host startswith "WS-"
    | stats requests = count(), hosts = dcount(src_host) by dest_domain
    | where hosts <= 3 and requests >= 200
    | sort hosts asc, requests desc
    -- PostgreSQL / DuckDB style; table proxy_events holds normalised events
    SELECT dest_domain,
           COUNT(*)                 AS requests,
           COUNT(DISTINCT src_host) AS hosts
    FROM   proxy_events
    WHERE  ts >= CURRENT_TIMESTAMP - INTERVAL '30 days'
      AND  src_host LIKE 'WS-%'
    GROUP  BY dest_domain
    HAVING COUNT(DISTINCT src_host) <= 3
       AND COUNT(*) >= 200
    ORDER  BY hosts ASC, requests DESC;

    Example output (synthetic) โ€“ 7 of roughly 42,000 distinct destinations survive the filter:

    dest_domain                       requests  hosts
    tiles.weather-widget.example          4320      1
    cdn-sync.example.net                   960      1
    img.newsfeed.example                   230      1
    status.backup-vendor.example         17280      2
    telemetry.cad-plugin.example           640      2
    api.label-printer.example             1210      3
    login.partner-portal.example           410      3

    The thresholds are judgement calls: "three hosts or fewer" defines rare for this fleet, "200 requests or more" removes one-off visits. Record them so the next hunter can reproduce or change them deliberately.

  4. Measure timing for the survivors only. For each host and candidate domain over the last seven days, compute the gaps between consecutive requests, ignore gaps longer than an hour (nights, lunch, a laptop lid closed) and calculate the CV.
    -- PostgreSQL / DuckDB style
    WITH seq AS (
      SELECT src_host, dest_domain,
             EXTRACT(EPOCH FROM ts - LAG(ts) OVER (
               PARTITION BY src_host, dest_domain ORDER BY ts)) AS gap_s
      FROM   proxy_events
      WHERE  ts >= CURRENT_TIMESTAMP - INTERVAL '7 days'
        AND  dest_domain IN ('tiles.weather-widget.example', 'cdn-sync.example.net',
                             'img.newsfeed.example', 'status.backup-vendor.example',
                             'telemetry.cad-plugin.example', 'api.label-printer.example',
                             'login.partner-portal.example')
    )
    SELECT src_host, dest_domain,
           COUNT(gap_s)                                               AS gaps,
           ROUND(AVG(gap_s)::numeric, 0)                              AS mean_s,
           ROUND(STDDEV_SAMP(gap_s)::numeric, 0)                      AS sd_s,
           ROUND((STDDEV_SAMP(gap_s) / AVG(gap_s))::numeric, 3)       AS cv
    FROM   seq
    WHERE  gap_s IS NOT NULL AND gap_s < 3600
    GROUP  BY src_host, dest_domain
    HAVING COUNT(gap_s) >= 20
    ORDER  BY cv ASC;

    Example output (synthetic):

    src_host     dest_domain                     gaps  mean_s  sd_s     cv
    WS-IT-02     status.backup-vendor.example    2015     300     1  0.003
    WS-IT-07     status.backup-vendor.example    2015     300     1  0.003
    WS-RCPT-01   tiles.weather-widget.example    1007     600     2  0.003
    WS-FIN-044   cdn-sync.example.net             475     300    11  0.037
    WS-SHP-03    api.label-printer.example        212     184   161  0.875
    WS-ENG-11    telemetry.cad-plugin.example     148     902   411  0.456
    WS-MKT-09    img.newsfeed.example              51     611   939  1.537

    Four rows look like timers (CV well below 0.1). The gap counts add a clue: the backup console and weather widget run around the clock, but WS-FIN-044 shows 475 gaps โ€“ exactly 95 per working day for five days โ€“ so its timer runs only while someone is logged on.

  5. Investigate every timer. Use context, not intuition. IT confirms the backup vendor's console ("refresh every 5 minutes"); facilities confirms the lobby display's weather widget. For WS-FIN-044 you pivot into EDR network-connection data to see which process talks to the domain:
    # pseudo-query: which process opens the connections?
    from edr_network
    | where host == "WS-FIN-044" and remote_domain == "cdn-sync.example.net"
    | stats connections = count(), first = min(time), last = max(time)
            by process_path, process_signer, parent_path

    Example output (synthetic):

    process_path                                          process_signer  parent_path                 connections  first                 last
    C:\Users\m.keller\AppData\Local\SyncHelper\synch.exe  (unsigned)      C:\Windows\explorer.exe            960  2026-06-17T07:58:40Z  2026-06-30T16:41:12Z

    An unsigned executable in a user-writable folder, started by Explorer at logon, present on one machine in the fleet, first seen two weeks ago, posting small requests to an uncategorised domain every five minutes โ€“ and nobody recognises it. You do not need certainty: this needs incident response.

  6. Hand over, then record the outcomes. You open an incident ticket with the evidence and pause the hunt. Incident response takes over (in this scenario they isolate the host and later confirm an unauthorised implant delivered through a fake invoice). Then you record the remaining outcomes: two benign timers added to a documented allowlist with an owner and a review date; the warehouse VLAN logged as a visibility gap with a request to route it through the proxy; and a detection handover so that a scheduled analytic repeats steps 3 and 4 every week.
Pro tip

Before you compute anything clever, sort by the boring columns. The gap count and the first-seen date in the example above told you more about WS-FIN-044 than the CV did: a timer that runs only during working hours and a binary that appeared two weeks ago are both strong, explainable signals you can put in an incident ticket.

What the handover to detection engineering looks like

A query that worked once is not yet a detection: it needs an owner, a schedule, a false-positive profile and a response. Hand it over as a short specification:

FieldContent for this hunt
NameRegular web requests to rare destination from workstation
ATT&CKT1071.001 Application Layer Protocol: Web Protocols
Data sourceProxy events (workstations); EDR network events for enrichment
Logic30-day prevalence โ‰ค 3 hosts, โ‰ฅ 200 requests; 7-day CV < 0.1 with โ‰ฅ 20 gaps under one hour
ScheduleWeekly, Monday morning; results to the hunt queue, not the alert queue
Known benignAllowlisted destinations with owner and review date (backup console, lobby display)
ResponseIdentify process via EDR; unexplained unsigned process โ†’ incident ticket

Note the schedule: low-volume, high-context analytics often belong in a weekly review queue, not the 24/7 alert queue. Lesson 10 covers when a hunt graduates into a real-time rule.

Second hunt: password spraying in cloud sign-in logs

The method does not depend on proxy logs. Here it is on identity data, which matters most once e-mail and files live in the cloud. Password spraying (ATT&CK T1110.003) means trying one or two common passwords against many accounts, which stays under per-account lockout thresholds.

Hypothesis: "Adversaries may be password-spraying accounts in our cloud identity provider (T1110.003), visible in the last 14 days of sign-in logs as a single source IP address that fails for many distinct accounts with only one or two attempts each." The data check confirms that successful and failed sign-ins are both exported. In this synthetic data set, result code 50126 means "invalid user name or password", as it does in Microsoft Entra ID sign-in logs; other providers use other codes.

Stacking needs two levels this time: first per source and account, then per source. SQL expresses that cleanly:

-- PostgreSQL / DuckDB style; signin_events holds normalised sign-in logs
WITH per_user AS (
  SELECT ip_address, user_principal,
         COUNT(*) AS attempts, MIN(ts) AS first_ts, MAX(ts) AS last_ts
  FROM   signin_events
  WHERE  ts >= CURRENT_TIMESTAMP - INTERVAL '14 days'
    AND  result_code = 50126
  GROUP  BY ip_address, user_principal
)
SELECT ip_address,
       SUM(attempts)  AS failures,
       COUNT(*)       AS users,
       MAX(attempts)  AS max_per_user,
       MIN(first_ts)  AS first_seen,
       MAX(last_ts)   AS last_seen
FROM   per_user
GROUP  BY ip_address
HAVING COUNT(*) >= 20
ORDER  BY users DESC;

Example output (synthetic):

ip_address     failures  users  max_per_user  first_seen           last_seen
198.51.100.23       412    388             2  2026-06-23 02:10:44  2026-06-23 03:55:09
192.0.2.10          263    191             3  2026-06-17 07:02:11  2026-06-30 17:48:40
203.0.113.77         58     29             2  2026-06-25 11:20:03  2026-06-25 11:24:51

Read the rows as stories. 198.51.100.23 failed against 388 accounts in under two hours at night, never more than twice each โ€“ the textbook shape of a spray. 192.0.2.10 is the company's own office address (shared behind NAT), with failures spread over two weeks of working hours: ordinary typos. 203.0.113.77 hit 29 accounts in five minutes โ€“ until the onboarding calendar shows new hires signing in for the first time.

The decisive question for a spray is never "did it happen?" but "did it work?". Pivot from the suspicious source to successful sign-ins:

# pseudo-query: any success from the spraying source?
from signin
| where ip_address == "198.51.100.23" and result_code == 0
| project time, user_principal, app, client_protocol, mfa_result
time                 user_principal       app    client_protocol  mfa_result
2026-06-23 03:41:17  r.huber@example.com  Mail   IMAP             not required

One account fell through a legacy mail protocol exempt from multi-factor authentication. That is an incident: reset the password, revoke sessions, review access and block legacy authentication. The improvement outcome is a detection counting distinct failed accounts per source in a short window, plus an alert on any success from a source that just sprayed.

What went wrong if you see this instead

The query returns no rows at all

Before concluding "no spraying", check that failed sign-ins are actually exported โ€“ some configurations forward only successful or only interactive sign-ins โ€“ and that the result code field is named and typed the way your query expects. A string '50126' compared with the number 50126 can silently match nothing, depending on the platform.

Many addresses with exactly one failure each

A distributed spray rotates source addresses so that no single IP stands out. Grouping by IP fails here. Group instead by time bucket and by the set of targeted accounts, or by user agent, and look for a burst of single failures across many accounts in the same hour.

Investigating outliers without fooling yourself

Stage 4 is where hunts are won or lost: every item on the short list needs a defensible label. The tree below is deliberately simple, because under time pressure simple rules get followed.

Decision tree for labelling a hunting outlier An outlier box leads to a question: is there a known owner and business reason. Yes leads to document as baseline with owner and review date. No leads to a second question: does it show malicious traits such as unsigned, user-writable path, newly seen or masquerading. Yes leads to escalate to incident response now. Unclear leads to gather more context within the timebox, and if still unclear when the timebox ends, escalate as suspicious with notes. Outlier Known owner and business reason? yes Document baseline owner + review date no Malicious traits? unsigned ยท user path ยท new ยท rare yes Escalate to IR now evidence in ticket unclear Gather more context within the timebox Escalate as suspicious when time is up
Figure 3. Labelling an outlier. Every item on a hunt's short list ends in one of three places: a documented baseline, an incident, or a suspicious escalation with notes. "I looked and it seemed fine" is not one of them.

Three habits keep this honest. Ask the owner, not your intuition โ€“ their one-sentence explanation goes into the record. Define a positive before you look โ€“ otherwise every data set contains something suspicious. Label everything โ€“ a skipped outlier returns next month and costs the same time again.

Common mistakes โ€“ and better habits

Common mistake
  • Scrolling through raw events hoping something jumps out.
  • Trusting an empty result without checking coverage.
  • Hunting for an IOC list and calling it a hunt programme.
  • Allowlisting a "known good" domain forever.
  • Continuing to hunt after finding live attacker activity.
Better
  • Aggregate first: stack, count, rank, then read the few events that matter.
  • Run a coverage check in stage 2 and record gaps as outcomes.
  • Hunt behaviours (techniques) and use indicators only as pivots.
  • Allowlist with an owner, a reason and a review date.
  • Stop, hand over to incident response, finish the record later.

Troubleshooting: when a hunt goes sideways

SymptomLikely causeFix
Thousands of "rare" destinationsContent delivery networks and per-customer subdomains make every host name uniqueStack on the registered domain (using the public suffix list), then drill into host names
The obvious timer shows a high CVOvernight gaps included, or the client adds random jitter to its intervalCap gaps (for example under one hour), use the median and median absolute deviation, or plot a histogram of gaps
Events from two sources are hours apart for the same actionOne source logs local time, the other UTCNormalise every timestamp to UTC at ingest and state the time zone in the hunt record
EDR shows no process for a domain the proxy sawEDR retention is shorter than proxy retention, or a system service made the requestCheck retention first; then use DNS query logs (for example Sysmon Event ID 22) to tie the domain to a process

How experienced hunters work

Experienced hunters rarely credit a better query language. They credit discipline โ€“ a rhythm you can copy from day one:

  • First 15 minutes โ€“ frame. Write down what a positive result looks like, where it lives and which fields you need. If you cannot, refine the hypothesis first.
  • Next 30 minutes โ€“ verify the data. Coverage per day, field population, retention, time zone โ€“ the cheapest time in the whole hunt.
  • Then โ€“ go from wide to narrow. Aggregate across the whole scope, filter to a short list, and only then open individual events. Each query goes into the hunt journal with its row count and one line of interpretation.
  • Explain the top, sample the middle. Every item at the rare end gets a label. A few items from the common end get a spot check โ€“ that is how you catch an attacker who hides in plain sight on many machines.

Advanced hunters have their own traps: confirmation bias (over-reading weak signals so two days "pay off"), tuning for tidiness (tightening filters until the outliers you wanted disappear) and single-source verdicts (a finding deserves corroboration from a second source, such as EDR for a proxy hit, before it is called an incident).

Where time goes in a typical first hunt (illustrative โ€“ not measured data)
Framing & data checks30%
Querying & stacking20%
Investigating outliers35%
Record & handover15%

Defender's view: make your environment huntable

Hunting is only as good as its telemetry. Both hunts here depended on earlier decisions: a logging proxy for all workstations, EDR recording which process opened a connection, failed cloud sign-ins exported and kept. These investments make every future hunt cheaper.

Data sourceWhat it lets you huntMinimum to collect
Endpoint process creationExecution, persistence, living-off-the-land toolsProcess, command line, parent, user, hash, signer (EDR, Sysmon Event ID 1 or Windows 4688 with command line)
Endpoint network and DNSWhich process talks to which destinationEDR network events or Sysmon Event IDs 3 and 22
Web proxy / secure web gatewayC2 over web protocols, uploads, rare destinationsAll workstation traffic, host name, bytes in and out, user, time in UTC
Authentication (on-premises)Lateral movement, credential abuseDomain controller events such as 4624, 4625, 4768, 4769
Cloud identity and control planeSpraying, token abuse, new credentials, logging changesSign-in logs (success and failure), directory audit logs, cloud provider API logs

Two settings illustrate how small these investments can be. On Windows, process-creation auditing and command-line capture are off by default. In a lab machine you can enable both from an elevated prompt (in production you would set the same options by Group Policy):

:: enable auditing of process creation (Security event 4688)
auditpol /set /subcategory:"Process Creation" /success:enable

:: include the full command line in event 4688
reg add "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit" /v ProcessCreationIncludeCmdLine_Enabled /t REG_DWORD /d 1 /f

:: verify
auditpol /get /subcategory:"Process Creation"

Example output of the verification command:

System audit policy
Category/Subcategory                      Setting
Detailed Tracking
  Process Creation                        Success

Then start notepad.exe, find event 4688 in the Security log and confirm the command-line field is filled. A setting not verified in the data is not coverage.

Privacy is part of the design

Proxy, sign-in and endpoint data describe what employees do. Collect what hunts need, restrict access, keep retention purposeful and document the legal basis with your data-protection contact.

Check your understanding

Check yourself

1. Which statement best describes threat hunting?

  1. Responding to alerts as quickly as possible
  2. A proactive, human-led search for adversary activity that existing detections have not reported
  3. Running an automated vulnerability scan every week
  4. Searching for a list of known-bad file hashes
Show answer

B โ€“ hunting starts from a hypothesis, not an alert; hash searches are IOC sweeps.

2. Which of these is a testable hunting hypothesis?

  1. "Look for anything unusual on the network."
  2. "Check whether we have malware."
  3. "Adversaries may password-spray our cloud identity provider, visible as one source failing for many distinct accounts in 14 days of sign-in logs."
  4. "Hunt for PowerShell."
Show answer

C โ€“ it names behaviour, scope, observable and data source.

3. Your coverage check shows that 41 workstations never appear in the proxy logs. What is the correct outcome?

  1. Ignore them; they are only a few percent of the fleet
  2. Record a data gap and request that their traffic be logged
  3. Cancel the hunt
  4. Assume they are compromised
Show answer

B โ€“ a visibility gap is a legitimate outcome; the hunt continues for the hosts you can see.

4. Gaps between requests from one host to one domain have a mean of 300 s and a standard deviation of 9 s. What is the coefficient of variation, and what does it suggest?

  1. 0.03 โ€“ very regular, timer-driven traffic
  2. 33 โ€“ very irregular, human traffic
  3. 0.3 โ€“ mixed traffic
  4. It cannot be computed from these values
Show answer

A โ€“ CV is standard deviation divided by mean: 9 / 300 = 0.03. A timer โ€“ which still needs an explanation.

5. During a hunt you find an unsigned binary in a user folder beaconing to a rare domain that nobody can explain. What do you do next?

  1. Continue the hunt to find more examples before telling anyone
  2. Delete the binary from the host
  3. Open an incident ticket with the evidence and hand over to incident response
  4. Add the domain to the allowlist
Show answer

C โ€“ a likely live threat goes to incident response; deleting or waiting harms the response.

6. A spraying source failed against 388 accounts. What is the most important next query?

  1. Count failures per account again with a different threshold
  2. Look up the source address in a public reputation list
  3. Check for successful sign-ins from the same source
  4. Delete the failed sign-in events to save storage
Show answer

C โ€“ the decisive question is whether the spray worked. A single success turns an attempt into a compromised account.

You can nowโ€ฆ

  • Define threat hunting โ€“ and tell it apart from triage, incident response, IOC sweeps and detection engineering.
  • Write a testable hypothesis โ€“ behaviour, scope, observable, data source.
  • Stack by prevalence โ€“ in pseudo-syntax and in SQL.
  • Measure timing regularity โ€“ with gaps, mean, standard deviation and CV.
  • Hunt for password spraying โ€“ with two-level aggregation and the "did it work?" pivot.
  • Hand a hunt over to detection engineering โ€“ as a specification, not a pasted query.

Glossary

Threat huntingProactive, human-led search for adversary activity that existing detections have not reported.
HypothesisA testable statement naming a behaviour, a scope, an observable pattern and a data source.
StackingGrouping events by a value and counting occurrences to find rare values; also called least frequency of occurrence.
PrevalenceHow many distinct hosts or users show a value, such as a domain or a file hash.
Coefficient of variationStandard deviation divided by mean; near 0 for regular timing, above 1 for bursty timing.
BeaconingRegular check-in traffic from software to a remote server; legitimate for many tools, suspicious when unexplained.
Password sprayingTrying a few common passwords against many accounts to avoid per-account lockouts (ATT&CK T1110.003).
A hunt is a structured experiment: a testable hypothesis, verified data, aggregation before inspection, a label for every outlier and an improvement at the end โ€“ whether or not you find an attacker.Next, Lesson 2 shows how to write hypotheses that are sharp enough to test and small enough to finish.

What's next in this course

  1. Lesson 2 โ€“ Hypothesis-Driven Hunting: Writing Testable Hypotheses: templates, scoping rules and how to rank a backlog of ideas.
  2. Lesson 3 โ€“ ATT&CK- and Intelligence-Driven Hypotheses: from a threat report or technique page to a hunt you can run this week.
  3. Lesson 4 โ€“ Hunting Data Sources: Endpoint, DNS, Proxy, Authentication and Cloud: the fields that matter and how to measure coverage.
  4. Lesson 5 โ€“ Baselining Normal Behaviour and Finding Anomalies: robust statistics that survive messy real-world data.
  5. Lesson 6 โ€“ Stacking and Frequency Analysis: Working the Long Tail: prevalence, rarity and multi-field stacks.
  6. Lesson 7 โ€“ Query Craft: Pseudo-Queries, SQL and Sigma for Hunters: patterns that translate between platforms.
  7. Lesson 8 โ€“ Hunting Endpoints and Networks: Persistence, Execution and Beaconing: complete hunts on process, DNS and network data.
  8. Lesson 9 โ€“ Hunting in Cloud and Identity Telemetry: sign-ins, tokens, mailbox rules and control-plane changes.
  9. Lesson 10 โ€“ Hunt Documentation and Handover into Detections: hunt records, specifications and when a hunt becomes a rule.
  10. Lesson 11 โ€“ Hunt Metrics, Maturity and Team Planning: measuring value honestly and running a hunt calendar.
  11. Lesson 12 โ€“ Capstone & Next Steps: a multi-day hunt from start to finish and 20 practice questions.

Lessons 2โ€“12 and the 40-question final test unlock with a one-time purchase โ€“ no subscription. Passing the final test earns a course completion certificate from Mobile Security Lab, which is not an industry certification.

Frequently asked questions

What is threat hunting?

Threat hunting is the proactive, human-led search for adversary activity that existing detections have not reported. It starts from a hypothesis about attacker behaviour rather than from an alert, and it assumes that a capable adversary could be present without triggering any rule.

How do you write a threat hunting hypothesis?

Use one sentence with four parts: the behaviour (ideally an ATT&CK technique), the scope (systems, users, time range), the observable pattern you expect, and the data source that would contain it. If you cannot name a data source, the hypothesis is not testable yet.

What is stacking in threat hunting?

Stacking, also called least frequency of occurrence, means grouping events by a value such as a domain, process or scheduled task and counting occurrences and distinct hosts. Values seen on very few hosts form the long tail, which is where unexplained activity often hides.

How can you detect beaconing in proxy logs?

First stack destinations by prevalence to find rare domains contacted many times. Then compute the gaps between consecutive requests per host and domain and their coefficient of variation (standard deviation divided by mean). Values near zero indicate timer-driven traffic, which you then explain or escalate.

Is a threat hunt that finds nothing a failure?

No. A well-designed hunt ends in a finding, a data gap, a documented baseline or a new or improved detection. Finding an attacker is the rarest outcome; the other three improve defences on almost every hunt.

How do you hunt for password spraying?

Aggregate failed sign-ins per source and account, then per source: a spray shows one source failing for many distinct accounts with one or two attempts each. The decisive follow-up is to check for successful sign-ins from the same source.

Continue with lessons 2โ€“12

One-time purchase, no subscription. Pay as a guest โ€” no account needed.

Unlock all 12 lessons ยท CHF 9.90 one-time Open in the course reader

Course syllabus

Lessons unlock after a single one-time purchase โ€” no subscription. Pricing & FAQ โ†’ ยท All course overviews โ†’