TH2.2 The make-series Operator

4-5 hours · Module 2 · Free
Operational Objective
Event logs record discrete events. Hunting for behavioral anomalies requires continuous time series — a value at every time interval, including intervals where nothing happened. The make-series operator transforms discrete events into time series that can be analyzed for trends, seasonality, and anomalies. This subsection teaches make-series with hunting-specific applications and the bin size decisions that determine whether anomalies are visible or buried.
Deliverable: The ability to use make-series to construct per-entity time series from M365 event data, with appropriate bin sizes and fill strategies for hunting analysis.
⏱ Estimated completion: 25 minutes

From events to time series

A sign-in log records that j.morrison signed in at 14:32, 14:47, and 15:03. Those are three events. A time series representation of the same data — with 1-hour bins — says: “hour 14 had 2 sign-ins, hour 15 had 1 sign-in, hour 16 had 0, hour 17 had 0…” The time series includes the zeros. The zeros are the hours where nothing happened — and in hunting, the change from “nothing happening” to “something happening” (or vice versa) is often the signal.

make-series is the KQL operator that performs this transformation.

Basic make-series for hunting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Per-user sign-in volume as a daily time series over 30 days
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| make-series SignInCount = count()
    on TimeGenerated
    from ago(30d) to now()
    step 1d
    by UserPrincipalName
// Each row = one user
// SignInCount = array of 30 values, one per day
// TimeGenerated = array of 30 timestamps (one per day)
// The array includes days with zero sign-ins
// This is the input format for series_decompose_anomalies()

The critical parameters:

step — the bin size. This determines the granularity of the time series. step 1d produces daily bins. step 1h produces hourly bins. step 5m produces 5-minute bins.

Smaller bins reveal finer-grained patterns but produce longer arrays and more noise. Larger bins smooth out noise but may hide short-duration anomalies. The correct bin size depends on the technique:

  • Authentication anomalies (TH4): 1-hour bins. AiTM token replay produces bursts of sign-ins within hours. Daily bins would average out the burst.
  • Data exfiltration (TH8): 1-day bins. Exfiltration typically sustains over hours or days. Hourly bins produce noise from legitimate work patterns.
  • C2 beaconing (TH12): 5-minute or 15-minute bins. Beacon intervals are measured in minutes. Hourly bins would miss the periodicity.

from and to — the time boundaries. Always specify both. Without them, make-series infers boundaries from the data — which means different users may have different time ranges, making comparison impossible.

by — the entity dimension. by UserPrincipalName creates a separate time series per user. by DeviceName creates one per device. by IPAddress creates one per IP. The entity dimension determines what you are profiling.

Fill strategies

By default, make-series fills gaps with zero. In most hunting applications, this is correct — a user who did not sign in on Tuesday has zero sign-ins for that day.

For metrics where zero is not the right default (e.g., session duration, where zero is meaningless), use default=:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Average session duration per day  fill gaps with null, not zero
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| extend SessionMinutes = datetime_diff(
    'minute', TimeGenerated, TimeGenerated)
| make-series AvgDuration = avg(SessionMinutes)
    default=real(null)
    on TimeGenerated
    from ago(30d) to now()
    step 1d
    by UserPrincipalName
// null = no data for that day (user did not sign in)
// 0 = user signed in but session duration was zero
// The distinction matters for anomaly detection

make-series for population profiling

Instead of per-entity series, create a single series for the entire population to understand organizational patterns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Organization-wide sign-in volume  daily pattern
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| make-series OrgSignIns = count()
    on TimeGenerated
    from ago(30d) to now()
    step 1d
// One row, one array of 30 daily values
// The pattern shows weekday/weekend cycles, holidays, trends
// This is the organizational baseline that per-entity anomalies
//   are measured against in some campaign modules
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Hourly pattern for one week  reveals the work day shape
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| make-series HourlySignIns = count()
    on TimeGenerated
    from ago(7d) to now()
    step 1h
// 168 values (7 days × 24 hours)
// The pattern shows: peak hours, off-hours volume, overnight baseline
// Activity outside the normal work pattern is a hunting signal
MAKE-SERIES — BIN SIZE DETERMINES WHAT YOU CAN SEEstep 1d (DAILY)30 values over 30 daysShows: day-level trendsMisses: hourly burstsBest for: TH8 exfiltration, TH13 insiderstep 1h (HOURLY)720 values over 30 daysShows: work pattern, off-hoursMisses: minute-level periodicityBest for: TH4 auth anomaliesstep 5m (5-MINUTE)8,640 values over 30 daysShows: beacon periodicityExpensive: large arrays, slow queriesBest for: TH12 C2 beaconingWrong bin size = hidden anomalies. Too small = noise. Too large = averaged out.Match the bin to the technique's expected temporal pattern.

Figure TH2.2 — Bin size selection for make-series. The technique being hunted determines the appropriate granularity. Each campaign module specifies its bin size.

Try it yourself

Exercise: Build your first time series

Run the organization-wide hourly sign-in query (168 values for one week). The result is an array. To visualize it, add `| render timechart` at the end of the query.

Observe: can you see the weekday/weekend pattern? Can you identify normal work hours versus off-hours? Is there a baseline overnight volume (service accounts, automated systems)?

Then run the per-user daily query for the top 5 users by sign-in volume. Compare their patterns. Do they follow the organizational pattern, or do some users have anomalous schedules?

This visual inspection is the foundation for the automated anomaly detection in TH2.3.

⚠ Compliance Myth: "Time series analysis requires specialized data science tools — KQL cannot do it"

The myth: Meaningful time series analysis requires Python, R, or a dedicated analytics platform. KQL is a query language, not an analytics tool.

The reality: KQL’s make-series family of operators provides built-in time series construction, decomposition, anomaly detection, forecasting, and smoothing — all within the Sentinel or Advanced Hunting query engine. No external tools required. The series_decompose_anomalies() function (TH2.3) is a production-grade anomaly detection algorithm that runs directly in KQL. For most hunting applications, KQL’s built-in time series capabilities are sufficient. Python (via Sentinel notebooks) is valuable for complex multi-variate analysis, but the standard hunting patterns in this course use KQL exclusively.

Extend this operator

make-series supports multiple aggregations in a single call: `make-series Count=count(), AvgDuration=avg(Duration), UniqueIPs=dcount(IPAddress)`. This produces parallel time series arrays that can be analyzed together — for example, detecting when both sign-in count and IP diversity increase simultaneously (stronger AiTM indicator than either alone). TH4 uses this multi-metric pattern.


References Used in This Subsection

You're reading the free modules of this course

The full course continues with advanced topics, production detection rules, worked investigation scenarios, and deployable artifacts. Premium subscribers get access to all courses.

View Pricing See Full Syllabus