A lab company, Contoso, patches a fresh CVE on Wednesday. On Friday the auditor asks a simple question: which devices were exposed, and when was each one fixed? The Defender portal answers with a polite, empty stare.
Rebuilt in my lab tenant with made-up data, checked against Microsoft Learn (October 2026). All names (Contoso, LAB-WIN11-01, app-mdvm-evidence) and numbers are lab examples. The diagram and the "portal" table are drawings, not screenshots.
TL;DR
- Vulnerability management in Defender shows now, not history. Once a device is patched it simply drops out of the exposed list, and
DeviceTvmSoftwareVulnerabilitiesonly has what is still vulnerable. - The proof lives in the delta export API: it returns every change with a
Statusof New, Fixed or Updated and anEventTimestamp. - Delta export only looks back 14 days, and nothing is kept for you. Pull it on a schedule and store it yourself, before the auditor shows up.
The case at a glance
| Situation | Contoso lab tenant, 6 devices onboarded to Defender for Endpoint Plan 2, one new CVE on 4 of them |
| Goal | Patch within the week, then hand the auditor a list: device, CVE, first seen, fixed on |
| What went wrong | After patching, the portal, advanced hunting and Resource Graph all showed "nothing". No history, so no proof |
| Fix | An Entra app with Vulnerability.Read.All, a script that pulls the delta export, keeps the Fixed rows and saves them every day |
| Result | A CSV per day with exposure and fix events, plus a weekly full snapshot as a baseline |
Table of contents
- 1. The situation
- 2. What I tried first, and why it failed
- 3. Why it happens
- 4. The fix, step by step
- 5. The result
- Lessons learned
- Gotchas
- Checklist
- Sources
1. The situation
The lab:
| Item | Lab value |
|---|---|
| Tenant | Contoso lab tenant |
| License | Defender for Endpoint Plan 2 (vulnerability management included) |
| Devices |
LAB-WIN11-01 to LAB-WIN11-04, LAB-SRV22-01, LAB-SRV22-02
|
| The CVE | One made-up browser CVE, found on the four Windows 11 devices |
| Roles | Me as Security Administrator, plus an Application Administrator to register an app |
Monday: the CVE shows up on four devices. Wednesday: the browser update goes out and all four are patched. Friday: the auditor, coffee in hand, asks for the list.
2. What I tried first, and why it failed
Try 1: the portal. Vulnerability management → Weaknesses → the CVE. Exposed devices: 0. Great news for security, useless for the audit. A device that is fixed is no longer exposed, so it is no longer listed.
Try 2: advanced hunting.
DeviceTvmSoftwareVulnerabilities
| where CveId == "<CVE-ID>"
| project DeviceName, SoftwareName, SoftwareVersion, CveId
Zero rows. Same reason: the table holds the vulnerabilities that are on devices now.
Try 3: Azure Resource Graph. Also current state only.
Three tools, three empty answers. The auditor's coffee is getting cold.
3. Why it happens
The vulnerability data in Defender is state-based. Microsoft Learn says it directly for the export APIs: the data is the current snapshot and doesn't contain historic data, so if you want history you must save it in your own storage.
The one place that talks about changes is the delta export of the software vulnerabilities assessment API:
| Full export | Delta export | |
|---|---|---|
| Endpoint | GET /api/machines/SoftwareVulnerabilitiesByMachine |
GET /api/machines/SoftwareVulnerabilityChangesByMachine?sinceTime=... |
| What you get | Everything vulnerable right now, per device | Only what changed since sinceTime
|
| Extra fields | none |
Status (New, Fixed, Updated) and EventTimestamp
|
| Refresh | Every 6 hours | After each full snapshot |
| How far back | n/a (now) |
sinceTime up to 14 days
|
Status = Fixed means the vulnerability no longer exists on that device. That row is your evidence.
4. The fix, step by step
Step 1. Register an app that can read vulnerability data
- Entra admin center (or Azure portal) → App registrations → New registration:
app-mdvm-evidence. - API permissions → Add a permission → APIs my organization uses → WindowsDefenderATP.
-
Application permissions →
Vulnerability.Read.All→ Add permissions, then Grant admin consent. - Add a credential. A certificate is better than a secret; in the lab I used a short-lived secret stored outside the script.
Gotcha: request the token for https://api.securitycenter.microsoft.com/.default. Some Defender for Endpoint APIs still expect that legacy audience and answer 403 Forbidden otherwise, even though you call api.security.microsoft.com.
Step 2. Pull the delta export
export-vuln-delta.ps1 does four things:
- Signs in as the app with Az.Accounts and gets a token for
https://api.securitycenter.microsoft.com. - Calls
SoftwareVulnerabilityChangesByMachinewithsinceTimeset to the last run (never more than 14 days ago). - Follows
@odata.nextLinkuntil all pages are read. - Saves all rows as JSON, and the
Fixedrows as a CSV: device, CVE, software, version, first seen, event time.
.\export-vuln-delta.ps1 -TenantId '<TENANT-ID>' -AppId '<APP-ID>' -SinceDays 1 -OutDir 'D:\mdvm-evidence'
Step 3. Keep a baseline too
Once a week, save a full export (SoftwareVulnerabilitiesByMachine) next to the deltas. Microsoft's own recommended pattern is exactly this: a full snapshot as the baseline, deltas in between. The full export also answers "what is still open?" on the day of the audit.
Step 4. Run it on a schedule
Run the script every day (Task Scheduler, an Azure Automation runbook, or a Logic App). Daily is safe: the data refreshes every 6 hours, and the 14-day window gives you room if one run fails. Write the files somewhere with retention, not on someone's desktop.
Step 5. Answer the auditor
From the Fixed CSV:
-
Exposed since:
FirstSeenTimestamp -
Fixed on:
EventTimestamp(good to about 6 hours, because of the refresh interval) -
Time to fix:
EventTimestamp - FirstSeenTimestamp
5. The result
In the lab (made-up numbers): the CVE appeared on 4 devices on Monday, the next day's delta showed 4 New rows, and after Wednesday's patch the delta showed 4 Fixed rows. The evidence CSV had one row per device with both timestamps, and the "time to fix" column read roughly two days for each.
| Before | After | |
|---|---|---|
| "Which devices were exposed?" | Unknown after patching | 4 devices, from the New rows |
| "When was each fixed?" | Unknown |
EventTimestamp of each Fixed row |
| Effort for the next audit | Panic | Open the folder |
Lessons learned
- Exposure views show now, not history. "Not in the portal" is not proof of anything.
- Decide your evidence source before the audit. The API exists, it is just easy to miss.
- 14 days is the hard limit. Miss two weeks of runs and those changes are gone from the delta.
- Store a weekly full snapshot. Deltas tell you what changed, the snapshot tells you what is.
Gotchas
-
Software updated but still vulnerable? You get two events:
Fixedfor the old version andNewfor the new one. Your report must not count that as fixed. -
Device moved to another device group? With the global view it can show up twice:
Fixedunder the old group,Newunder the new one. De-duplicate on device ID and record ID. -
Wrong token audience.
403 Forbiddenwith a valid token usually means the token was issued for the wrong resource. -
Rate limits. 30 calls per minute and 1,000 per hour for the JSON exports. Use a large
pageSize(up to 200,000) instead of many small calls. - Big tenants. Above about 100,000 devices, use the via files export for the full snapshot.
- Not in Sentinel. The TVM hunting tables aren't ingested into Microsoft Sentinel, so a Sentinel query against them returns nothing. Query them in Defender advanced hunting.
Checklist
- [ ] App registered with
Vulnerability.Read.All(application) and admin consent - [ ] Credential stored safely (certificate preferred), not in the script
- [ ] Token requested for
https://api.securitycenter.microsoft.com/.default - [ ] Daily delta export with
sinceTimewithin 14 days - [ ] Weekly full snapshot saved as a baseline
- [ ] Evidence stored with retention
- [ ] Report de-duplicates version changes and device-group moves
What's your evidence source when an auditor asks "prove it"? I'd like to hear what works for you.













