Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
0.4 Your First Investigation
Introduction
An investigation in this course starts with a question small enough to answer and specific enough that the answer can be wrong.
This lesson asks one: who does Northgate's domain serve on an ordinary working day? It is a question an administrator is asked in the first week of any job, and it is a good first one because every later module leans on knowing what normal looks like before deciding something is not.
You'll answer it the way every lesson in the course answers its questions, by choosing the evidence, writing the query, reading the rows, checking them a second way and writing down what you found.
The figure is the whole lesson in one picture. The corpus month holds 22,051 Security events, and each step of the funnel keeps the ones that bear on the question: the ticket-granting ticket requests, then the ones the controller granted, then the accounts behind them, then the people among those accounts. Each step is a line of a query you'll write.
Nothing here is a finding any later module turns on; it is the baseline those modules read their findings against, and it is the shape of every investigation that follows.
The Question, and Why 4768 Answers It
investigate: choosing the evidenceEvery account that does any work in an Active Directory domain starts its day by asking a domain controller for a ticket-granting ticket.
A person signing in at a laptop, the laptop itself when it starts, a service account when its service starts: each asks the Key Distribution Center on a controller to prove who it is, and gets a ticket that buys every other ticket for the next ten hours. The controller records that request as event 4768, and Microsoft's reference describes what each field means:
What a 4768 records
Microsoft's event 4768 reference, Security auditing on a domain controllerTwo properties make 4768 the right evidence for this question. It is recorded on the controller, so it doesn't depend on any laptop or server keeping its own log; and it happens once per account per sign-in rather than once per file opened, so counting accounts from it counts the population rather than its activity.
The event 4769, the service ticket, is the opposite: one account asks for dozens a day, one for every server it touches, and counting them measures how busy people were. Choosing the event that matches the question is the first decision in any investigation, and it is the decision most often skipped.
The obvious alternative, event 4624, an account was logged on, answers a different question. A 4624 is written by the machine the logon happened on, so a person signing in at a laptop produces one on the laptop, and the laptop's Security log is not what a SIEM usually collects.
The 4624s a controller writes are mostly network logons to the controller itself, such as a laptop reading Group Policy, which measure who touched the controller rather than who used the domain. 4768 is written where the decision is made, on the controller, for every account, wherever it signs in from.
The sequence shows why a count of accounts will hold computers as well as people. A Windows laptop joined to the domain has its own account, NE-ASHBY-LT$, with its own password that the machine changes every thirty days, and it signs in as itself when it starts, before anyone types a password.
Only then does a person sign in and ask for a ticket of their own. So a domain of 86 people with laptops should show at least that many computers asking too, and if it doesn't, either the laptops are off or the count is wrong.
Counting a Day
investigate: requests and accounts by dayStart with the broadest count that still answers the question: for every day in the month, how many granted ticket-granting tickets and how many distinct accounts behind them. Status "0x0" keeps the granted requests, bin() rounds each timestamp down to its day, and dcount() counts each account once however often it asked:
SecurityEvent
| where EventID == 4768 and Status == "0x0"
| summarize Requests = count(), Accounts = dcount(TargetUserName) by Date = bin(TimeGenerated, 1d)
| sort by Date asc
Thirty rows, one for each day from 14 February to 15 March. The working days are almost identical: 141 to 144 accounts and 182 to 186 granted requests on every one of the 20 weekdays.
The 10 weekend days hold 25 to 47 accounts each. Requests run a little higher than accounts because some accounts ask more than once in a day, which section 4 explains. One week of the month, drawn from those rows:
The chart is what a baseline looks like when it is healthy: flat where the business is flat and changing where the business changes.
A Monday with 90 accounts would mean something happened to the laptops or to the log; a Saturday with 140 would mean something was signing in that isn't a person going to work. Neither appears in the month, and that absence is part of the answer, because you now know what an odd day would look like before you need to recognize one.
One detail of the query decides what a day is. TimeGenerated is stored in UTC, and bin() cuts at UTC midnight, so a day in these results runs from midnight to midnight UTC. For a business in the UK in February and March that is also local midnight, but from the last Sunday in March the clocks go forward and every UTC day starts at 01:00 in Manchester.
A baseline compared across that change compares slightly different windows, and the morning burst moves an hour earlier in UTC. Write the time zone on the baseline, and you'll never mistake the clock change for a change in behavior.
The time of day says the same thing more sharply. Count the granted requests by the hour they were made, and keep the five busiest hours:
SecurityEvent
| where EventID == 4768 and Status == "0x0"
| summarize Requests = count() by Hour = datetime_part("hour", TimeGenerated)
| sort by Requests desc
| take 5
The hour from 07:00 alone holds 1,694 requests, and the four hours from 06:00 to 10:00 hold 98 per cent of the month's granted requests.
In February and March UTC is also the time in Manchester, so that is the morning arrival, as laptops start and people sign in. And 3,993 of the 4,104 requests came from 10.0.3.0/24, the workstation subnet the directory's site configuration places in Manchester-HQ, so the clock and the address agree about where the domain's day happens.
What Counts as an Account
investigate: computers, people, services and administratorsThe daily figure of about 142 accounts is an answer, but not yet to the question, which asked who. An account name says what kind of thing it is if the directory was built with a naming rule, and Northgate's was: computer accounts end in $, service accounts start with svc-, and administrators' separate admin accounts start with a-.
A case() expression applies those rules in order, and the month's accounts split as follows, in KQL and in SPL:
SecurityEvent
| where EventID == 4768 and Status == "0x0"
| extend Kind = case(TargetUserName endswith "$", "computer", TargetUserName startswith "svc-", "service", TargetUserName startswith "a-", "administrator", "person")
| summarize Accounts = dcount(TargetUserName), Requests = count() by Kind
| sort by Accounts desc
index=wineventlog sourcetype="WinEventLog:Security" EventCode=4768 Result_Code="0x0"
| eval Kind=case(like(Account_Name, "%$"), "computer", like(Account_Name, "svc-%"), "service", like(Account_Name, "a-%"), "administrator", true(), "person")
| stats dc(Account_Name) AS Accounts, count AS Requests BY Kind
| sort - Accounts
Both return the same four rows from the same corpus:
Kind Accounts Requests
person 86 1858
computer 49 2061
service 6 167
administrator 3 18
86 people, 49 computers, 6 service accounts and 3 administrator accounts, 144 in all. The person rule is a default rather than a test, which is a choice worth noticing: anything that doesn't match the other three patterns is counted as a person, so an account named outside the convention would land there.
In your own domain, check the person row's names before trusting its total, because naming rules are written by people and followed by most of them.
The directory holds a better test than the name, and it is the one to use where the naming rule is loose. A computer account has the object class computer; a service account usually carries a servicePrincipalName; an administrator's account sits in a privileged group.
Those are attributes rather than conventions, and the lessons that need certainty, such as Module 5's privileged account auditor, read them. For a first count of the population the name is enough, and it has the advantage of being in the event itself, so the query needs nothing but the log.
The requests column says how each kind behaves. People ask 1,858 times in the month, about once each working day; computers ask 2,061 times, more than the people do; service accounts ask 167 times between six of them, on their own schedules. Those rhythms are what later modules compare against when an account starts behaving like a different kind of account.
One Working Day, Counted
investigate: Wednesday 11 MarchA month-level count blends every day together, so the next step is to take one ordinary day and count it the same way. Wednesday 11 March is a working day in the middle of the month with nothing unusual in its totals, which is what you want for a baseline day. Write the query yourself; it is the month's query with one more line:
The answer is 86 people, 49 computers, 6 service accounts and 1 administrator, 142 accounts from 184 granted requests. Every person in the month signed in that day.
Every computer did too, and 20 of the 49 asked three times, because a laptop asks again for its own ticket when it restarts or comes back onto the network, which is the ordinary rhythm of a machine rather than a sign that something changed.
The service accounts are the row to read slowly, because each one keeps its own clock. Six accounts asked for 8 tickets that day. Across the month, each one's requests and the number of days it asked on:
SecurityEvent
| where EventID == 4768 and Status == "0x0" and TargetUserName startswith "svc-"
| extend Date = startofday(TimeGenerated)
| summarize Requests = count(), Days = dcount(Date) by TargetUserName
| sort by Requests desc
Two rhythms. Five accounts ask about once on each day they run, 20 to 23 requests over 20 to 23 days, which is the working week plus a few weekend days for the ones whose services never stop.
The sixth, svc-sql, asks twice a day, near 03:00 and 13:00, on all 30 days, 60 requests in the month: a scheduled job rather than anyone's working day. A service account that starts asking at a new hour, or from a new address, is one of the first things the detection modules look for, and this table is what they compare it to.
The administrator row is small on purpose. Northgate's administrators have separate accounts for administration and use them only when they administer, so on most days one or two appear and on many days none. A day with every admin account signing in from the morning onwards would be worth a question, and Module 5 is about why.
The Weekend
investigate: who works when nobody is workingThe weekend is where a baseline earns its keep, because the population is small enough to read by name. Run the same split across every Saturday and Sunday in the month together:
SecurityEvent
| where EventID == 4768 and Status == "0x0"
| where dayofweek(TimeGenerated) == 0d or dayofweek(TimeGenerated) == 6d
| extend Kind = case(TargetUserName endswith "$", "computer", TargetUserName startswith "svc-", "service", TargetUserName startswith "a-", "administrator", "person")
| summarize Accounts = dcount(TargetUserName), Requests = count() by Kind
| sort by Accounts desc
Across the 10 weekend days, 71 people, 48 computers and 4 service accounts asked at least once, and no administrator did.
That looks like a lot of people until you remember it covers 10 days: on any one weekend day it is 25 to 47 accounts in all, and about half of those are laptops that stayed on over the weekend and renewed their own tickets. The people are the ones who opened a laptop on a Saturday to finish something, which is how real offices look.
Two things here would matter in an investigation. First, the weekend has no administrator sign-ins at all in the month, so the first one would stand out against a background of none.
Second, the service accounts that run on weekends are fewer than on weekdays, four of the six, which tells you which services follow the working week and which run every day regardless. Both facts were invisible in the month-level count, and both came from asking the same question of a narrower slice. Narrowed once more, to the service accounts that asked on a Saturday or a Sunday:
Service accounts at the weekend
Successful 4768, the 10 weekend days of the corpus monthThe four weekend service accounts are worth naming, because they show the difference between a schedule and an occasion. svc-sql asked 20 times across the weekends, twice on every one of the 10 days, exactly as it does in the week. svc-sentinel asked 3 times, svc-mfp-scan 2 and svc-backup 1: a handful of weekend days each, which is what a service looks like when it runs when something needs it rather than on a timer.
Neither pattern is a problem. Each is a fact you can now compare against.
What the Count Leaves Out
investigate: refusals, missing logs and quiet daysEvery count has edges, and the investigator's job is to know where they are before someone else finds them. The first edge is the requests the controller refused. Count all 4768 by controller and status, without the Status filter:
SecurityEvent
| where EventID == 4768
| summarize Requests = count(), Accounts = dcount(TargetUserName) by Computer, Status
| sort by Requests desc
Three rows. NE-DC01 granted 2,758 requests and NE-DC02 granted 1,346; NE-DC01 also refused 17, each with status 0x6, which Microsoft's reference describes as a name the Kerberos database doesn't hold.
Refusals aren't part of who the domain serves, so the baseline is right to leave them out, but they are not nothing either. Lesson 1.6 reads them; here it is enough to know they exist and that the Status filter is what kept them out.
The second edge leaves no row at all. The count reads the controllers whose Security logs reach the SIEM, and only those.
A controller whose log nobody collects would grant tickets all month and contribute nothing, and no query against the collected data can show it, because there is nothing to query. The three questions below are the ones to ask before reading any count from domain controller logs as complete:
The third edge is the quiet day. A laptop left off for a week asks for nothing and drops out of that week's daily counts while staying in the month's, which is why the baseline is stated as a range across days rather than as one number.
Of the three, only the middle question can't be answered from the events, and it is answered by comparing the Computer column against the list of controllers in the directory, a check the Practice at the end of the lesson asks you to run on your own domain.
Writing the Answer Down
act: the baseline, dated and reproducibleAn answer that lives in a query window is gone the moment the window closes, so the last act of an investigation is to write down what was found, from what, and how to find it again.
A baseline needs four things to be useful later: the question, the evidence it was read from, the numbers with their ranges, and the edges it doesn't cover. Northgate's, as of the end of the corpus month:
Northgate's working day, as a baseline
Written 11 March from the corpus month; KQL and SPL return the same rowsEvery number on the card came from a query in this lesson, and every query is saved beside it, so anyone can rerun it next month and compare. The "Not counted" line is the most important one on the card, and the one most often left off.
A reader who sees 142 accounts on a working day will assume that is every account; the card says which controllers it read, so a reader can tell the difference between "the domain served 142 accounts" and "two collected controllers granted tickets to 142 accounts", which are not the same claim.
Saving the query is the act that makes the card reproducible. In Sentinel a query saved as a function is called by its name like a table; in Splunk a report can run on a schedule and write its rows to a lookup, so each month's result sits beside the last without anyone copying it:
Logs > paste the query from section 3 > Save > Save as function
Function name: NE_TgtBaselineByKind
Legacy category: BaselinesNE_TgtBaselineByKind
| where Kind == "service"Save As > Report: "NE TGT baseline by kind"
Reports > expand the row > Schedule: Edit > Schedule Report
Schedule: Run on Cron Schedule, 0 6 1 * * (06:00 on the first of each month)
Add Actions > Output results to lookup: ne_tgt_baseline.csvStore with the queries: the date, the question, the evidence, the ranges, what is not countedThe card also commits to a schedule. A baseline written once and never refreshed becomes a comparison against a domain that no longer exists, and the most common way an investigation goes wrong is by measuring today against a normal that was true a year ago. Monthly, and after any change to the controllers or to what is collected, is the rhythm the course uses for every baseline it builds.
Checking It on the Controllers' Own Logs
verify: the same day, a different toolA result read once through one tool is a result you hope is right. The check is to get the same answer by a route that shares as little as possible with the first one.
The SIEM count came from events that were collected, parsed and stored; the controllers' own Security logs, read with Get-WinEvent, come from the source before any of that happened. Here is 11 March, read from both controllers' exported logs and grouped twice, first by controller and status and then by kind:
PS C:\> $tgt = foreach ($dc in "NE-DC01", "NE-DC02") {
>> Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = "Security"; Id = 4768; StartTime = (Get-Date "2026-03-11"); EndTime = (Get-Date "2026-03-12") } | ForEach-Object {
>> $x = [xml]$_.ToXml(); $d = @{}; $x.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.InnerText }
>> [pscustomobject]@{ DC = $dc; Account = $d.TargetUserName; Status = $d.Status } } }
>> $tgt | Group-Object DC, Status | Format-Table Count, Name -AutoSize
>> $tgt | Where-Object Status -eq "0x0" | Select-Object -Unique Account |
>> Group-Object { switch -Wildcard ($_.Account) { "*$" { "computer" } "svc-*" { "service" } "a-*" { "administrator" } default { "person" } } } |
>> Sort-Object Count -Descending | Format-Table Count, Name -AutoSize
Count Name
----- ----
120 NE-DC01, 0x0
1 NE-DC01, 0x6
64 NE-DC02, 0x0
Count Name
----- ----
86 person
49 computer
6 service
1 administrator
The export holds 185 events from 11 March: 184 granted and 1 refused, and the four kinds come out as 86, 49, 6 and 1, the same rows the KQL and SPL returned.
The command is shown as you would type it on an analyst workstation with read access to the controllers' Security logs; its output comes from the lesson's evidence file, l4-dc-tgt-0311.xml, which is that day's 4768 events exported in the XML shape wevtutil writes. The file is in the free Module 0 evidence folder, so you can run the same parse yourself.
Reading a controller's Security log from another machine needs two things the SIEM route never asks of you.
The account running the command must be allowed to read the log, which on a domain controller means membership of Event Log Readers, the built-in group whose members, in Microsoft's words, can read event logs from local computers, rather than membership of Domain Admins. And the controller's firewall must allow the remote event log service, because Get-WinEvent reaches it directly rather than through PowerShell remoting.
An investigator who signs in to a controller with an admin account to run this check has made a sign-in that Module 1 teaches you to question.
Three tools now agree on one day: KQL in Sentinel, SPL in Splunk and PowerShell on the logs themselves. That is what verified means in this course.
When a later lesson says a detection returns two rows, or a change made an event stop, it has been checked the same way, and when the tools disagree, the disagreement is a finding of its own, usually about what one of them was never sent.
Practice
- Choose two weeks. Pick a recent fortnight with no holiday in it, so working days and weekends are both ordinary.
- Count by day. Run the daily query against your own Security events from the domain controllers, and read the weekdays against the weekend.
- Count by kind. Split the accounts with the same case() rule, then adjust it to your naming: your service accounts and administrators may not start with svc- and a-.
- Check the edges. List your domain controllers and confirm every one appears in the Computer column; a controller missing from the results is a log you are not collecting.
- Write it down. A card like the one in section 7, dated, with the query beside it, kept where you will find it next month.
Next, lesson 0.5: one detection written four ways, in KQL, SPL, PowerShell and Sigma, and why the course keeps all four.