In this section

Catching Generated Query Errors: Your First Drill

Module 0

Four generated queries. Each one runs. Each one returns rows. Three of them are wrong and one is fine, and the point of the fourth is that a habit which flags everything is as useless as one that flags nothing.

Work them in order. For each: read the request, read the query, decide before you run it, then run it and see whether the data agrees with you.

Before you start

Everything so far has been argument. This is the first time you do the thing.

The four queries below were generated from the requests shown above them. They are realistic: each is the kind of query you get back in four seconds, each is syntactically valid, each references real columns in the Northgate corpus, and each returns rows. None of them fails in a way you can see from the output.

Two rules make this drill work.

Decide before you run. Write down, even as one word, whether you think the query is sound and which of the six modes it is exposed to. An unwritten judgment adjusts itself to the answer without your noticing, and you will finish believing you caught things you did not.

Run them. The blocks are editable and execute against the live corpus. Reading that a query returns 31 rows is a different experience from watching 31 rows appear when you expected none.

Worth Knowing

The corpus is anchored in March 2026

Northgate's telemetry covers a thirty-day window ending 15 March 2026. That is a fact about this environment and it matters for the first drill, in exactly the way a fact about your own estate would matter for a query somebody generated for you. Estate context is not an abstraction; it is the difference between a right answer and a plausible one.

Drill 1

The request

"An alert fired on r.scott. Show me the outcome of every sign-in attempt on that account over the last week so I can see whether the attack succeeded."

SigninLogs
| where UserPrincipalName == "r.scott@ne.com"
| where TimeGenerated > ago(7d)
| summarize Attempts = count() by ResultType

Decide first. Sound, or which failure mode?

Run it. You get one row: ResultType 0, with 31 attempts. Thirty-one successful sign-ins, no failures at all, which reads as a quiet week on a healthy account.

The Reasonable Mistake

Reading a clean result as evidence the attack failed

This is a silent window. The corpus is anchored in March and ago(7d) is measured from now, so the window sits entirely after the incident. The eighty-four attempts on the night of 2 March, of which eighty-three failed, are outside it.

Nothing in a result of 31 suggests a window problem. Remove the time filter entirely and the same query returns 130 sign-ins with three distinct result codes. The filter was doing the work, not the analysis.

The check: move the boundary deliberately. If widening the window changes the answer's shape rather than its size, the boundary was the finding.

Drill 2

The request

"That Lithuanian address hit r.scott hard. Did it touch anyone else? Show me the failures it caused, by account."

SigninLogs
| where IPAddress == "45.83.64.117"
| where ResultType != 0
| summarize Failures = count() by UserPrincipalName

Decide first.

Run it. One row: r.scott@ne.com, 83 failures.

Worth Knowing

This one is correct, and that matters

The analyst asked which accounts the address caused failures on. The query filters to that address, excludes successes because failures were what was asked for, and groups by account. Every clause maps to something in the request.

The single row is a real finding: this address went after one account rather than spraying. If you flagged it as suspicious because the previous drill was wrong, you have learned something about your own calibration rather than about the query.

Drill 3

The request

"We think c.richardson's session token was stolen. Has that account authenticated at all since the phishing email?"

SigninLogs
| where UserPrincipalName == "c.richardson@ne.com"
| where ResultType != 0
| summarize Attempts = count()

Decide first.

Run it. 4.

The Reasonable Mistake

Answering a question adjacent to the one asked

Two failures at once. The analyst asked whether the account authenticated; the query filters ResultType != 0, which counts only the attempts that FAILED. That is right answer, wrong question.

Worse, a stolen session token does not produce an interactive sign-in at all. Token replay is recorded in AADNonInteractiveUserSignInLogs, where this account has 779 events. Querying SigninLogs for token abuse is a confident absence waiting to happen: the wrong table returns a small number rather than an error.

The check: name the semantic claim. For this to answer whether the account authenticated, the filter would have to include successes, and the table would have to be the one that records this kind of authentication. Both fail.

Drill 4

The request

"Give me the count of distinct users that Conditional Access did not apply to, so I can size the gap."

SigninLogs
| where ConditionalAccessStatus == "notApplied"
| summarize Events = count()

Decide first.

Run it. 12.

The Reasonable Mistake

A count of the wrong thing, reported as the right thing

The analyst asked for distinct users. The query counts events. Twelve is a real number and it is not the number requested, and if it goes into a report as "twelve users affected" it is invented precision introduced by the analyst rather than by the model. Change count() to dcount(UserPrincipalName) and the answer changes.

What the drill was measuring

Not whether you spotted three errors. Anyone primed by a module on failure modes will be suspicious of everything.

What it measured is whether you passed drill 2. An analyst who flags the correct query has a habit that will be abandoned within a fortnight, because a colleague who distrusts every generated answer has stopped getting value from the tool and everyone around them knows it.

The working target is not maximum suspicion. It is naming which of the six a query is exposed to, running the one check that settles it, and moving on in under a minute.

Practice & resources
Included with your plan

Everything you have read here can be practiced against the same estate: graded SOC scenarios, forensic cases, Splunk and AWS query drills, a free-run KQL and SPL console, and the response playbooks.

Open the Practice Hub →