In this section

0.3 The Data This Course Hunts In

Module 0

Every hunt in this course reads tables, and each table is written by a particular product, holds a particular kind of event and stamps time in a particular column. A hunter who knows which table holds which rows can go straight to the evidence; one who does not spends the first hour of every hunt finding out.

This lesson is the map: the tables the course uses, how many rows each holds in the month, what one row looks like, how the window is applied, and how the same people appear under different names in different tables.

Northgate's data lives in two places. Defender XDR holds what its products see on the laptops, in mail and in identity, and its advanced hunting reads those tables directly. The Sentinel workspace holds the cloud sign-in logs, the audit logs, and the feeds from servers, domain controllers and the firewall.

Microsoft notes that "In the unified Microsoft Defender portal, you can run queries over Microsoft Sentinel tables by onboarding a workspace", which is how Northgate is set up, so one query can read both.

The two platforms differ in one detail that matters in every query: the name of the time column. Defender XDR tables stamp events in Timestamp; Sentinel tables in TimeGenerated. A query that filters on the wrong one fails or, worse, silently filters nothing. Every query in the course names the right column for each table it reads.

The lesson counts and reads ordinary rows only. The month's intrusions sit in these same tables, and the modules that teach the techniques to find them open them one at a time.

Hunt brief
TriggerBefore your first hunt you need to know where Northgate's rows are kept, how they are stamped, and how the tables relate.
QuestionWhich tables does the course hunt in, what do they hold, and how is each filtered and joined?
DataNine Defender XDR tables; nine Sentinel tables; IdentityInfo
Window13 February 12:00 to 15 March 12:00 UTC, half-open
PlatformDefender portal: Defender XDR tables on Timestamp; Sentinel tables on TimeGenerated
Step 1

Count the Defender XDR tables

Rows in the window for each table the course reads, on Timestamp

Microsoft's schema reference describes each Defender XDR table in a line: DeviceProcessEvents holds "Process creation and related events", DeviceNetworkEvents "Network connection and related events", DeviceLogonEvents "Sign-ins and other authentication events on devices", and so on. The nine the course reads, counted:

let start = datetime(2026-02-13 12:00);
let stop = datetime(2026-03-15 12:00);
union
    (DeviceProcessEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "DeviceProcessEvents"),
    (DeviceNetworkEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "DeviceNetworkEvents"),
    (DeviceFileEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "DeviceFileEvents"),
    (DeviceLogonEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "DeviceLogonEvents"),
    (DeviceEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "DeviceEvents"),
    (EmailEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "EmailEvents"),
    (EmailUrlInfo
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "EmailUrlInfo"),
    (UrlClickEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "UrlClickEvents"),
    (IdentityLogonEvents
        | where Timestamp >= start and Timestamp < stop
        | summarize Rows = count()
        | extend Table = "IdentityLogonEvents")
| project Table, Rows
| order by Rows desc

The nine Defender XDR tables, with their rows in the window, largest first:

Table                Rows
-------------------  -----
EmailUrlInfo         21209
EmailEvents          15188
DeviceProcessEvents  9394
DeviceNetworkEvents  5884
DeviceFileEvents     4279
DeviceEvents         3573
IdentityLogonEvents  2523
DeviceLogonEvents    1928
UrlClickEvents       1204

65,182 rows across nine tables. The endpoint tables, Device-prefixed, come from Defender for Endpoint on the laptops and the servers that have it. EmailEvents, EmailUrlInfo and UrlClickEvents come from Defender for Office 365: messages delivered or blocked, the links inside them, and the clicks on those links.

IdentityLogonEvents comes from Defender for Identity on the domain controllers, and Microsoft describes it as "Authentication events on Active Directory and Microsoft online services".

A count per table is also the first sanity check on any data source. A table that should hold thousands of rows and holds a dozen has a broken feed; a table that should be quiet and holds millions has a noisy one. Northgate's counts are in proportion to what each table records, which is what a healthy workspace looks like.

The sizes reflect what each table records. EmailUrlInfo writes a row for every link in every message, so it is the largest table even though mail is only one part of the estate. A process row is written every time any program starts on a covered device, so the process and network tables follow.

Click rows exist only when someone follows a link in a message, so they are few. A small table is not a less important one; UrlClickEvents is small, and Module 7's phishing hunts turn on it.

Defender XDR also holds tables this course does not read, about devices, alerts, vulnerabilities and more, and the schema reference lists them all. The nine here are the ones that hold the activity hunts are built on.

A table left out of a hunt is not a table forgotten; it is one whose rows would not answer the question, and part of planning a hunt is saying which tables are in and why.

Two of the nine are worth a note for anyone coming from the endpoint side. DeviceEvents is a catch-all for events that do not fit the other device tables: attack surface reduction audits, access to sensitive processes, driver loads and other actions the sensor records with an ActionType that says what happened.

EmailUrlInfo holds the links extracted from each message, one row per link, which is how a hunt finds every message that carried a given domain without reading the bodies.

The Defender XDR tables are also where the course's endpoint hunts get their precision. Because every process start is a row with its parent, its account and its command line, a hunt can ask very specific questions, such as which programs Word started this month or which accounts ran PowerShell remotely, and get an exact answer.

That precision is what the alert stream cannot offer, and it is the main reason the endpoint tables appear in more of the course's hunts than any others.

Widen The Defender side is counted. Widen to the Sentinel workspace.

Step 2

Count the Sentinel tables

Rows in the window for each workspace table the course reads, on TimeGenerated

The workspace collects the logs Defender XDR does not hold. The nine the course reads, counted on their own time column:

let start = datetime(2026-02-13 12:00);
let stop = datetime(2026-03-15 12:00);
union
    (SigninLogs
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "SigninLogs"),
    (AADNonInteractiveUserSignInLogs
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "AADNonInteractiveUserSignInLogs"),
    (AuditLogs
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "AuditLogs"),
    (OfficeActivity
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "OfficeActivity"),
    (SecurityEvent
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "SecurityEvent"),
    (Syslog
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "Syslog"),
    (CommonSecurityLog
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "CommonSecurityLog"),
    (SecurityAlert
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "SecurityAlert"),
    (SecurityIncident
        | where TimeGenerated >= start and TimeGenerated < stop
        | summarize Rows = count()
        | extend Table = "SecurityIncident")
| project Table, Rows
| order by Rows desc

The nine Sentinel tables, with their rows in the window, largest first:

Table                            Rows
-------------------------------  -----
OfficeActivity                   15960
AADNonInteractiveUserSignInLogs  13332
SigninLogs                       8062
Syslog                           5244
CommonSecurityLog                2060
SecurityIncident                 1262
SecurityEvent                    1109
SecurityAlert                    698
AuditLogs                        521

48,248 rows across nine tables. SigninLogs holds interactive sign-ins and AADNonInteractiveUserSignInLogs the token refreshes and background sign-ins that happen without a person typing. AuditLogs records directory changes, OfficeActivity mailbox and SharePoint operations. SecurityEvent carries Windows Security events from the domain controllers and some servers, Syslog the Linux servers' logs, and CommonSecurityLog the firewall. SecurityAlert and SecurityIncident are the alerts and incidents 0.1 counted.

The two sign-in tables are a common trap. A person who signs in once in the morning produces one interactive row and then, through the day, many non-interactive rows as their applications refresh tokens. A hunt that reads only SigninLogs sees the first and misses the rest, and an attacker using a stolen token appears mostly in the non-interactive table. The capstone lab depends on reading both.

Two platforms, one portal Defender XDR advanced hunting Time column: Timestamp . 65,182 rows in the window EmailUrlInfo 21,209 EmailEvents 15,188 DeviceProcessEvents 9,394 DeviceNetworkEvents 5,884 DeviceFileEvents 4,279 DeviceEvents 3,573 IdentityLogonEvents 2,523 DeviceLogonEvents 1,928 UrlClickEvents 1,204 Microsoft Sentinel workspace Time column: TimeGenerated . 48,248 rows in the window OfficeActivity 15,960 AADNonInteractiveUserSignInLogs 13,332 SigninLogs 8,062 Syslog 5,244 CommonSecurityLog 2,060 SecurityIncident 1,262 SecurityEvent 1,109 SecurityAlert 698 AuditLogs 521 Defender portal queries both, in UTC Every query names its tables' own time column. A query that mixes the two platforms filters each on its own.

The figure is the map the rest of the course assumes. When a lesson names a table, it is one of these eighteen, and its time column is the one shown on its side. Module 3 goes deeper into each, including the columns that matter most for hunting and the ones that are empty in this tenant.

Each workspace table has its own way of naming things, and learning them is part of learning the data. AuditLogs records who made a change in a JSON column called InitiatedBy and what was changed in another called TargetResources. OfficeActivity names the user in UserId and the source address in ClientIP.

SecurityEvent identifies each Windows event by an EventID, the same number a Windows administrator knows from the event viewer. Syslog carries the Linux process name and the raw message. None of this needs memorizing; Module 3 and each lesson introduce the fields when a hunt first needs them.

The workspace also holds the alert and incident tables, which are a different kind of table from the rest. They record what the detection products concluded, not what happened. A hunt uses them for context and corroboration, as 0.1 did, and never as a substitute for the activity tables underneath.

Check coverage The tables are mapped. Check what one row actually holds.

Step 3

Read one row field by field

An ordinary process row from a staff laptop

A table is easier to hunt in once one of its rows has been read slowly. An ordinary row from a staff laptop on a working Wednesday, a document opened from Outlook in Word:

DeviceProcessEvents
| where DeviceName == "NE-DAVIS-LT" and InitiatingProcessFileName == "OUTLOOK.EXE"
| where Timestamp between (datetime(2026-03-11 11:46) .. datetime(2026-03-11 11:47))
| project Timestamp, DeviceName, ActionType, FileName, ProcessCommandLine,
    AccountName, AccountUpn, InitiatingProcessFileName, ReportId

The row's fields, one per line, as the query projected them:

One process row, read field by field

Timestamp

2026-03-11T11:46:49.241Z

DeviceName

NE-DAVIS-LT

ActionType

ProcessCreated

FileName

WINWORD.EXE

ProcessCommandLine

ffice16\WINWORD.EXE" /n "C:\Users\Public\Attach\spec.docx"

AccountName

a.davis

AccountUpn

a.davis@ne.com

InitiatingProcessFileName

OUTLOOK.EXE

ReportId

806246

DeviceProcessEvents records each process creation. This row is a person opening an attachment from Outlook in Word.

Each field answers a question a hunter asks. Timestamp says when, in UTC. DeviceName says where. ActionType says what kind of event: ProcessCreated here. FileName and ProcessCommandLine say what started and with what arguments, here Word opening a document from the Public attachments folder.

AccountName and AccountUpn say who, in the on-premises and cloud forms of the same name. InitiatingProcessFileName says what started it, Outlook, which is what makes this an attachment being opened rather than a person launching Word from the Start menu. ReportId identifies the event within the device's report.

The values also show what the row does not say. It does not say whether the document was safe, whether it came from inside or outside the company, or what Word did after it opened.

Those answers are in other rows: the mail table for where the attachment came from, the file table for what Word wrote, the process table again for anything Word started. A hunt assembles a picture from many rows, and each row contributes the few facts it records.

The parent field is the one the course leans on most. Almost every endpoint hunt asks what started a process, because the same program means different things under different parents: Word under Outlook is a person opening an attachment, and PowerShell under Word is something else entirely. Module 5 builds on exactly this.

Reading a row slowly is a habit worth keeping long after this lesson. Many hunting mistakes are misreadings of a single field: an AccountName that is the service the process ran as rather than the person at the keyboard, a LogonType that means a network share rather than a person signing in, an address that belongs to a proxy rather than a client.

The cure is the same each time: before building on a field, read what it holds in a handful of ordinary rows.

Check coverage One row is read. Check how the window is applied to a table.

Step 4

See the half-open window at work

A table stamped at the window's excluded end

The course's window includes its start and excludes its end: Timestamp at or after 13 February 12:00, and before 15 March 12:00. Most tables are unaffected by the distinction, but one shows it vividly. The directory snapshot, IdentityInfo, counted with and without the window:

IdentityInfo
| summarize Rows = count(),
    InsideWindow = countif(TimeGenerated >= datetime(2026-02-13 12:00)
        and TimeGenerated < datetime(2026-03-15 12:00)),
    Stamped = max(TimeGenerated)

The directory snapshot, counted once in full and once with the course's window applied:

Rows  InsideWindow  Stamped
----  ------------  ----------------
97    0             2026-03-15 12:00

97 rows, and none inside the window, because every row is stamped 2026-03-15 12:00, exactly the excluded end. A hunt that applies the window to IdentityInfo gets nothing and might conclude the directory is empty. The course therefore queries IdentityInfo without the window, and the lessons that use it say so.

Microsoft writes that "Advanced hunting uses UTC (Coordinated Universal Time) for all data" and that the portal converts results to the viewer's time zone for display. The course writes every datetime in UTC and shows results in UTC, so the window's 12:00 is noon UTC on both dates, and a time in a result can be compared with any other without conversion.

The broader lesson is about reading silence. An empty result has three possible meanings: nothing happened, the query cannot see what happened, or the query is filtering it away. This example is the third kind, and it looks exactly like the first. The course's rule, repeated from Module 4 onward, is that an empty result is not a finding until the query has been shown able to return a row.

The window's two edges have different jobs. The start, inclusive, makes sure the first event of the month is counted once. The end, exclusive, makes sure that two consecutive windows, this month and next, never count the same event twice.

The course uses one window for everything, so the second job rarely matters here, but a hunter who reruns a hunt monthly needs both edges right or will double count at the joins.

Snapshot tables like IdentityInfo are different from event tables in a way that matters beyond the window. An event table grows as things happen; a snapshot describes a state at one moment.

A hunt that asks what an account's job title was when something happened needs the title at that time, and a single snapshot only has the title at the end. In a month this rarely changes the answer, but the record should say which kind of table a fact came from.

Check coverage The window is understood. Check how the platforms name the same people.

Step 5

Join the same people across platforms

Accounts that sign in to the cloud, matched to logons on endpoints

The cloud and the endpoints name a person differently: SigninLogs uses the full sign-in name, j.smith@ne.com, while DeviceLogonEvents uses the account name alone, j.smith. A join across the platforms has to make the keys match. Staff accounts that signed in, matched to their endpoint logons:

SigninLogs
| where TimeGenerated >= datetime(2026-02-13 12:00)
    and TimeGenerated < datetime(2026-03-15 12:00)
| where UserPrincipalName !has "#EXT#"
| summarize SignIns = count() by Account = tostring(split(UserPrincipalName, "@")[0])
| join kind=leftouter (DeviceLogonEvents
    | where Timestamp >= datetime(2026-02-13 12:00)
        and Timestamp < datetime(2026-03-15 12:00)
    | summarize Logons = count() by Account = AccountName) on Account
| summarize Accounts = count(), WithEndpointLogons = countif(Logons > 0),
    CloudOnly = countif(isempty(Logons) or Logons == 0)

The staff accounts that signed in, and how many of them also have activity on a covered endpoint:

Accounts  WithEndpointLogons  CloudOnly
--------  ------------------  ---------
93        16                  77

The join itself is a small piece of KQL worth reading. split(UserPrincipalName, "@")[0] takes the part of the sign-in name before the @, which matches the account name the endpoints use. A leftouter join keeps every cloud account, matched or not, so the count of unmatched accounts is as visible as the matched ones. An inner join would have dropped them silently and made the coverage look complete.

93 staff accounts signed in to the cloud; 16 of them also logged on to a device that reports to Defender. The rest are cloud-only in this data: people whose laptops are not among the sensor-equipped devices, or who work only in the browser.

That is a coverage fact as well as a join lesson. For most staff, the course can see what they do in Microsoft 365 but not what runs on their machines, and hunts that need both are possible only for the sixteen.

Key mismatches are the commonest reason a cross-platform join returns too few rows. The cloud name and the account name are the simplest case. Device names are another: the sign-in logs carry a device identifier, the endpoint tables a device name, and the firewall an address.

Addresses are the least reliable key of all, because devices change addresses and several devices can share one. The course shows each case where it first matters and always prefers the most stable key a pair of tables shares.

Microsoft's reference for ResultType notes that "0 indicates success; other values are failures", and the join above counts only accounts, not outcomes. A hunt that joins sign-ins to logons for a purpose, say to find cloud sign-ins with no matching activity on the user's laptop, would also filter on the result, and would need the laptop to be one of the sixteen for the absence to mean anything.

Record

The hunt record

Outcome: proven

The record closes the data hunt. Proven here means each table was shown to hold rows in the window, so its silence in a later hunt means something.

Hunt recordProven
Finding

The course reads eighteen tables: nine in Defender XDR on Timestamp, 65,182 rows in the month, and nine in the Sentinel workspace on TimeGenerated, 48,248 rows. A process row records when, where, what, with what arguments, by whom and started by what. The window includes its start and excludes its end, and IdentityInfo, stamped at the end, must be queried without it. Joining cloud sign-ins to endpoint logons needs the sign-in name split at @; 16 of 93 staff accounts have endpoint activity.

Bookmarks
  • Defender XDR: 65,182 rows; DeviceProcessEvents 9,394
  • Sentinel: 48,248 rows; SigninLogs 8,062
  • Row: WINWORD.EXE under OUTLOOK.EXE on NE-DAVIS-LT
  • IdentityInfo: 97 rows, 0 inside the window
  • Join: 16 of 93 accounts on endpoints
Bounds

Counts and one ordinary row. The tables' columns in depth are Module 3's subject.

Next
  • Run a first hunt on these tables (0.4)
  • Read each table's columns in depth (Module 3)

Counting the tables is also the simplest proof that the data is there. Every count above is above zero, which means every table the course uses holds rows in the window. When a later hunt queries one of these tables and gets nothing, the emptiness is about the question, not about a missing feed, and that is what the record's outcome of proven refers to.

The map will be filled in as the course goes. Module 3 adds each table's columns, its quirks and what it cannot see. Module 4 teaches the language for asking these tables questions properly.

Modules 5 to 7 take the endpoint, identity and mail tables in turn, and Module 8 joins them. What this lesson fixes is the habit: name the table, use its time column, apply the window deliberately, and make the keys match before joining.

Each module adds tables to this map in the order the hunts need them. Module 5 lives mostly in the four endpoint tables and DeviceEvents. Module 6 reads SigninLogs, the non-interactive log, AuditLogs, IdentityLogonEvents and SecurityEvent. Module 7 reads the mail tables and OfficeActivity. Module 8 reads them together, with the firewall and Syslog. By the capstone every table on the map has been used for a real question.

The tables are also a reminder of what a hunter can and cannot ask. A question about who opened a document can be answered from the process table on a covered laptop; the same question about an uncovered laptop cannot be answered at all. Choosing a hunt starts with checking that its question can be answered from the tables available.

Do

Practice

How many rows does each table hold?
hands on

Steps 1 and 2 counted the tables on their own time columns. Build one count yourself, then try filtering a Sentinel table on Timestamp to see what happens.

Write down which platform each table belongs to and its time column. Then write what the wrong time column did.

Check

Read the evidence

Reading the window

The check gives you the directory snapshot counted with and without the window. Decide what the counts mean.

Resources

Publishers revise these pages; where a page and this lesson disagree, the page is newer.