Not as straightforward as I made it sound
It’s been a while. Yesterday morning I decided to do a little test. I had confidently stated in a meeting on Friday, that we could/should turn on change tracking. A lot of my writing here is really me trying to learn or understand something, so after a couple of hours I wanted to start documenting this journey.
Simply put, change tracking is easy to turn on, and once it’s on it reports changes beautifully. What it doesn’t do is tell you is who did this to my server!. Which, honestly, is a big part of the reason I suggested it could be turnd on in the first place.
Introduction to Azure Arc
If you don’t know Azure Arc, it is the service that lets you bring on-premises servers (and other resources) under the Azure control plane, so one can manage and monitor them with the same tools used for Azure-native workloads. I’ve helped clients deploy Arc in the past, but mostly as a modern way of deploying Windows updates – a modern replacement for WSUS or SCCM. But honestly the primary reason is to get Defender for Endpoint onto Servers which I’ve written about before.
There are quite a few areas that are worth exploring: SQL Managed Instance on-prem, on prem Kubernetes, unified Azure Policy across a hybrid estate and this is not about licencing, but some items are just free if you have Software Assurance.
And then Change Tracking and Inventory. Tracking who is doing what on the servers – especially catching people who bypass change management, either intentionally or just by quickly logging in to do “something innocuous” that seemed too small to need a change request. We’ve all seen this happen, and the uncomfortable truth is that change management only works if you can see the changes.
So, having mentioned it, I thought I’d turn in on in my home lab and woosh – there went my day.
Attempt one: set it up, test it, but where’s the who?
Turning Change Tracking on was about as easy as it looks. Arc was already installed on some of my lab servers. I enabled Change Tracking, let the default configuration do its thing, and installed Chrome as a test – as most of my lab servers only has Edge.

Within a couple of minutes the change was showing up in the Change Tracking portal. Under Added there was Google Chrome, alongside the three Windows Services that Chrome’s installer registers (Updater, Elevation, Updater Internal). Clicking View on the Chrome row opened the change history and dutifully showed me everything about what had just happened – previous value: Not Installed; current value: 147.0.7727.102; publisher: Google LLC; timestamp precise to the second.

What it didn’t show me was the thing I actually cared about: who installed it. No column for a user, no account name anywhere in the record, no field I’d missed. Same story over in Log Analytics – a quick ConfigurationChange | getschema confirmed that the table simply doesn’t carry a user identity. SourceSystem = OpsManager tells you the agent reported it, but the agent doesn’t know which human hit the installer.

It turns out, this is by design. Or maybe just bad design – how difficult could it be? But there are actually two different worlds of change data inside Azure, and they live in different places:
| Table | Lives in | What it tracks | Captures who? |
| resourcechanges | Azure Resource Graph | Changes made through the Azure control plane | Yes |
| ConfigurationChange | Log Analytics workspace | Changes made on the machine (via the agent) | No |
The first catches anything you do through the portal, CLI, SDK, or an ARM/Bicep deployment, and it knows who did it – changedBy, changedByType, clientType. The second catches what actually changed on the box – a software install, a service state change, a registry edit – but it has no idea which human was behind it, because it’s the agent watching the machine.
Change Tracking is the second table. So even with a perfectly-configured DCR, it was never going to answer “who?” on its own. So now what – I can see someone did something – but who?
Attempt two: the DCR is a shell

Change Tracking is driven by a Data Collection Rule (DCR) – the Azure Monitor object that says “collect this data from these machines and send it to that workspace”. When I opened the DCR that my environment had stood up by default, I found something sobering. All the suggested registry keys were listed but every one of them was disabled. There was no Windows Event Log collection configured. The only thing actually collecting anything was the automatic software inventory.
That’s why Chrome showed up – software inventory is on by default, independent of DCR configuration – and it’s why anything slightly more interesting would have slipped past unnoticed. The default DCR wasn’t a deliberate design. It was a shell.
The real answer to “who?”
Here’s the shift that changed my whole view of this. The “who” question isn’t actually a change-tracking question. Change tracking tells you what changed on the machine. Identity lives in the Security event log – specifically event 4624 (successful logon) and optionally event 4688 (process creation, if you’ve enabled it in audit policy).
You answer “who installed Chrome on this server at 06:42 UTC?” by correlating the change-tracking timestamp against whoever was logged on at that moment. Two tables, one question. Once you see it that way, the limitation stops looking like a bug and starts looking like a design choice – one that’s perfectly reasonable, but that you really need to know about before you try to use change tracking to tell you something it was never built to tell you.
Attempt three: there are two kinds of DCR
Armed with that insight, I built a new DCR through the Azure Monitor wizard, configured it with the Windows Event Log XPath queries I wanted, disassociated the default DCR from my server, and tried to associate the new one through the Change Tracking enable wizard.
Not a Change Tracking Data Collection Rule.
It turns out there are two flavours of DCR in play here, and they are not interchangeable for this purpose:
- Change Tracking DCR – created and configured through the Change Tracking UI. Holds the software inventory, Windows Services, registry keys, and file change configuration. Feeds the
ConfigurationChangetable. - Azure Monitor DCR – created through the Azure Monitor wizard. Holds the Windows Event Log XPath queries (and Performance Counter / Syslog / etc.). Feeds tables like
Event,SecurityEvent, orWindowsEventdepending on the data source type you choose.

You need both associated to the same machine. The Change Tracking enable wizard will only accept a Change Tracking DCR; the Azure Monitor wizard won’t produce one. So the right model is: keep the auto-generated CT DCR (mine is called something deeply memorable like ct-dcr-1669058987-43117934) and configure it properly through the Change Tracking UI, then create a separate Azure Monitor DCR for your event log collection and associate that alongside it.
Once I worked that out, things started behaving.
Building the production-standard collection
Rather than continue patching the half-configured defaults, the cleaner move is to design what I’d call a production-standard collection and make that the onboarding target for Arc servers. Every new machine then inherits the right scope automatically, and with a bit of Azure Policy you can enforce the association so there’s no room for drift.
Here’s the v1 scope I landed on, split across the two DCRs:

| Category | Where it lives | What I enabled |
| Software inventory | CT DCR | On (automatic) |
| Windows Services | CT DCR | On |
| Registry keys | CT DCR | A pragmatic subset of Microsoft’s recommended list (below) |
| Windows Event Log – Security | Azure Monitor DCR | 4624, 4625 via XPath |
| Windows Event Log – System | Azure Monitor DCR | 7045 via XPath |
Registry keys
For registry, Microsoft pre-stages a list of recommended keys in the Change Tracking UI – all of them disabled by default. Worth knowing: only HKEY_LOCAL_MACHINE is supported. HKCU doesn’t work, which is a small but real limitation if you’re hoping to catch per-user persistence.
I went with this subset from the recommended list:
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Group Policy\Scripts\ShutdownHKLM\Software\Microsoft\Windows\CurrentVersion\RunHKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunHKLM\Software\Microsoft\Active Setup\Installed ComponentsHKLM\System\CurrentControlSet\Control\Session Manager\KnownDllsHKLM\Software\Microsoft\Windows NT\CurrentVersion\Drivers32HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Drivers32HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\NotifyHKLM\Software\Microsoft\Windows NT\CurrentVersion\Group Policy\Scripts\Startup

That gives reasonable coverage of common persistence and OS-configuration tamper paths without lighting up everything Microsoft suggests. Important caveat: monitoring the keys tells you that one of them changed – it doesn’t tell you who changed it. That’s a separate piece of plumbing, which I’ll come back to in a section of its own below.
Windows event logs
For the event log side, in the Azure Monitor DCR wizard I chose Custom rather than Basic mode – Basic lets you pick broad severity categories (which pulls in a lot of noise), Custom lets you target specific event IDs with XPath queries. The two I’ve gone with are:
Security!*[System[(EventID=4624 or EventID=4625)]]System!*[System[(EventID=7045)]]

I deliberately left event 4688 (process creation) out of v1. It’s useful, but it can generate significant volume and requires audit policy to be enabled on the machines. Better to start narrow, see what the baseline looks like, and add it if there’s a real gap.
A note on which event table you end up in
This caught me out and is worth noting. Depending on the data source type you pick in the DCR, your Windows event data will land in one of three different tables:
- Event – the classic table. Generic schema, with a
RenderedDescriptionfield containing the full event text. This is what you get from a “Windows Event Logs (Custom XPath)” data source. Universally available. - SecurityEvent – the parsed Security log table, with proper columns for
Account,LogonType,IpAddress, etc. This is what you get from a “Security Events” data source – but that option is gated. It requires Sentinel or Defender for Servers Plan 2 to be attached to the workspace. Without that, the data source simply isn’t selectable. - WindowsEvent – the AMA-native table. Only populated if you use the right data source variant; not present in older workspaces.
I started without Sentinel attached, so I ended up parsing RenderedDescription from the Event table by hand. It works, it’s just less elegant. If you’re already running Sentinel, take the SecurityEvent route – it’s cleaner, and the same data doubles up for analytics rules and incidents.
Linking it all together – tying a software change back to a person
This is the bit that justifies all the attempts. Here’s the query that joins a change-tracking record to the logon session most likely to be responsible for it. It assumes the Event table route (parsing RenderedDescription) and looks for any interactive logon (LogonType 2 or RemoteInteractive 10) within the two hours before the change:
let changes = ConfigurationChange
| where TimeGenerated > ago(2h)
| project ChangeTime = TimeGenerated, Computer, ConfigChangeType, SoftwareName, Previous, Current;
let logons = Event
| where TimeGenerated > ago(4h)
| where EventID == 4624
| extend LogonType = toint(extract(@"Logon Type:\s+(\d+)", 1, RenderedDescription))
| where LogonType in (2, 10)
| extend AccountNames = extract_all(@"Account Name:\s+(\S+)", RenderedDescription)
| extend Account = tostring(AccountNames[1])
| extend IpAddress = extract(@"Source Network Address:\s+(\S+)", 1, RenderedDescription)
| project LogonTime = TimeGenerated, Computer, Account, LogonType, IpAddress;
changes
| join kind=inner logons on Computer
| where LogonTime <= ChangeTime and LogonTime > ChangeTime - 2h
| summarize arg_max(LogonTime, Account, LogonType, IpAddress)
by ChangeTime, Computer, ConfigChangeType, SoftwareName, Previous, Current
| sort by ChangeTime desc
A few things worth knowing about this KQL query:
The extract_all on Account Name is deliberate – the rendered 4624 text contains two Account Name fields, the subject (often MACHINE$ or SYSTEM) and the new logon account. Index [1] is the one you actually want. The LogonType in (2, 10) filter cuts out the noise from service and pseudo-account logons (DWM-3, UMFD-* and friends). The arg_max picks the most recent qualifying logon before each change – a decent proxy for “who was on the box at the time”.
If you’ve taken the SecurityEvent route, the same logic is much shorter – you don’t need the extract and extract_all, you just project Account, LogonType, IpAddress directly off the columns.
The proof
To prove the loop end-to-end I did two things on a freshly-logged-in RDP session as Administrator:
- Installed VS Code (User installer).
- Uninstalled 7-Zip.
Both showed up in ConfigurationChange within a few minutes – the install as a Software Added row, the uninstall as a matching Software Removed row alongside some helper-service churn from the uninstaller. When I ran the join query above, both rows came back paired with the same Administrator logon at LogonType 10 from my source IP. That is the answer the original meeting question was after – at least for software changes: not just what changed, but who was on the box at the time it happened.

Caveat worth being honest about: this is strong evidence, not proof. If two interactive sessions are open at the same time, you’d see both correlate to the same change and you’d need extra context (process creation, MDE telemetry) to narrow further. For a single-admin lab server this is more than good enough. For a busy multi-admin jump box, you’d want the extra signal.
What 4624 doesn’t tell you: registry change attribution
The 4624 correlation works really well for software installs and uninstalls because they almost always happen in the foreground under an interactive session. If Administrator was logged on at 14:55 and VS Code installed at 14:55:56, the correlation isn’t a guess – it’s the only plausible explanation.
For registry changes, the same logic falls apart. A registry value can change without any interactive logon at all – an installer running as SYSTEM, a scheduled task firing at 03:00, a service updating its own configuration, Group Policy applying a setting, a management agent doing housekeeping. Correlating a registry write to “the most recent interactive logon” might point at someone who logged off two hours ago and had nothing to do with the change. That isn’t attribution, it’s a guess.
If you actually want to attribute a registry change to a user account, 4624 isn’t enough. You need Event ID 4657 – “A registry value was modified” – which Windows writes into the Security log and which carries the account name, logon ID, process name, and the specific value changed.
The catch: 4657 isn’t on by default. It requires the Audit Registry policy to be enabled at Success/Failure under Advanced Audit Policy → Object Access. Until you turn that on, the events simply aren’t generated, and no DCR in the world will collect what doesn’t exist.
There are three ways to turn it on, and one of them is a lot cleaner than the others if you’re working in an Arc-first world:
Group Policy. Set Computer Configuration → Windows Settings → Security Settings → Advanced Audit Policy Configuration → Object Access → Audit Registry to Success and Failure. Fine if your machines are domain-joined and your GPO management is mature. Awkward for pure Arc-managed servers that don’t have a domain controller behind them.
Azure Policy / Machine Configuration. This is the Arc-native equivalent of Group Policy for audit settings. A Machine Configuration policy runs AuditPol.exe on the machines under the hood and applies the same Advanced Audit Policy across every Arc-enrolled server, without needing Active Directory in the picture. For a hybrid or pure-Arc estate, this is the right lever – and it slots neatly alongside the DCR association policy I’m already planning.
Defender for Endpoint. If MDE is already onboarded (which is often the case via Arc anyway), you don’t need 4657 at all. MDE has its own registry-change telemetry that lands in the DeviceRegistryEvents table in the Microsoft 365 Defender portal, complete with process and user context. If MDE is in play, query that first and only worry about 4657 if there’s a gap.
Either way, this is a two-stage problem.
- Stage one is detection – knowing that something changed in a watched key. The CT DCR with registry monitoring covers that.
- Stage two is attribution – knowing who changed it. That needs either Audit Registry pushed via Machine Configuration and the resulting 4657 events ingested into the workspace, or a pre-existing MDE deployment doing the job natively. The default Change Tracking experience gives you stage one and silently leaves stage two as homework.
That homework is a follow-up post in itself, and probably the right next thing to build on top of this one.
Collect what you need, make it a standard
The thing that surprised me most about all this isn’t that Change Tracking is complicated – it isn’t, sort of, once you’ve sorted the two-DCR confusion. It’s that the out-of-the-box experience gives you a service that looks like it’s working, says it’s working, reports changes when you test it, and then falls silent on the one question that matters. Change without identity is a log file. Change with identity is an audit trail. The default configuration gives you the first; getting to the second is a deliberate piece of design work, not a toggle.
So, the broader point – and I think the real takeaway – is this: collect what you need, and have a standard for change tracking, rather than default DCRs scattered all over the place. The default Change Tracking DCR is a placeholder. Treating it as a finished design is how you end up with change tracking that looks like it’s working when it isn’t really watching for anything interesting. Building a deliberate pair of DCRs once, associating them via Azure Policy at Arc onboarding, and reviewing their scope periodically, is a much cleaner operating model – and it’s the kind of thing that’s much easier to put in place before a hundred servers are already onboarded than after.
Even with the production-standard pair of DCRs in place, the full attribution story for registry changes needs another deliberate decision (Machine Configuration to enable 4657, or use MDE if you already have it). The point isn’t that any of these pieces is hard individually. The point is that the default experience leaves a lot unfinished, and joining it all up is the actual work.
Where I’m at, and what’s next
Production-standard CT DCR and Azure Monitor DCR are both built and associated. End-to-end correlation for software changes is proven. Three follow-ups sit on the back of this one:
- The Azure Policy that enforces the DCR association at Arc onboarding, so the production-standard collection follows every new Arc server automatically.
- The registry attribution path – a Machine Configuration policy to enable Audit Registry and ingest 4657, or a check on whether MDE is doing the job already.
- The Sentinel cut-over – with Sentinel attached, the
SecurityEventtable makes the join query trivially shorter, and the same collection investment pays for both forensics and detection. But I’m going to try and fix this without Sentinel first.
Azure Policy appeals but, in many cases, MDE is where the Arc journey started so I’m not promising to follow up on all routes.
As always, I’ve done my best to ensure this is correct but please always check for yourself.
S x

