ASCII art banner for the duplicate device records Microsoft Defender for Endpoint scanner script

Hunting Duplicate Device Records in Microsoft Defender

Last Updated on February 20, 2026 by Oktay Sari

ā€œThe same kind of script is sorely needed for Defender. It uses hostname as the unique identifier for Macs so any device name change creates a new record and uses up a license. It’s bonkers.ā€

That was Luke on LinkedIn, commenting on my Entra ID duplicate scanner. And he was absolutely right. MDE’s device inventory can feel like a ghost town of orphaned records.

So there I was, staring at the device inventory in security.microsoft.com, counting duplicate device records like sheep before bed. Except these sheep weren’t helping me sleep. They were keeping me up at night. Before I knew it, it was 3 AM for the next couple of days, and another PowerShell script was born. My wife thanks you too, Luke. 🤠

This project won’t completely solve Luke’s problem. You still can’t delete device records from Microsoft Defender, and the 180-day retention window is non-negotiable. But what it does do is give you visibility into the mess, automatically identify which records are the ghosts, tag them so they’re easy to filter out, and optionally exclude them from your vulnerability management reports.

TL;DR: MDE creates duplicate device records every time a device is renamed, reimaged, or re-onboarded, and you can’t delete them. This PowerShell script groups devices by HardwareUuid, scores each record across 21 signals to identify orphans, and tags them via the official API. Optionally, it generates a helper script that can bulk-exclude flagged devices from Vulnerability Management using undocumented portal APIs (proof of concept, requires XDRInternals). GitHub link

 

šŸ“Œ Update (February 20 2026): Shortly after publishing this post, Nathan McNulty shared that native device exclusion cmdlets are coming to XDRInternals. The Set-XdrEndpointDeviceExclusionState cmdlet is currently in a preview branch and will make the manual approach in this post significantly simpler. The concepts and research below still apply, but keep an eye on XDRInternals for the official release! 🤠

Table of Contents

A Quick Note on Licensing

Before we dive in, let’s address the licensing elephant in the room. Luke mentioned duplicate records ā€œusing up a license.ā€ Now technically, MDE licensing is per user, not per device (up to five devices per user). The license usage report is calculated based on users who signed in to onboarded devices, not the number of device records in the inventory. So duplicate ghost records that nobody signs into shouldn’t directly eat a license.

However, they do clutter your inventory, inflate device counts, and make license usage reports harder to interpret. And let’s be honest, nobody wants to explain to their CISO why the portal shows 347 devices when you only manage 200. šŸ‘€

What This Project Actually Is (and Isn’t)

I want to be upfront about something. This project has two very different halves.

The scanner and tagging workflow? That’s built on solid foundations. It uses official, documented Microsoft APIs with proper OAuth authentication. The building blocks are all there for anyone who wants to integrate this into their automation workflows or adapt it for scheduled runs.

Fair warning: I’m a fan of verbose output. Some might say too verbose. The main scanner has 395 Write-Host statements across 2,736 lines of code. That’s roughly one log message every 7 lines. If your terminal could file a restraining order, mine probably would. But hey, when something goes wrong at 3 AM, you’ll thank me for it. šŸ˜…

ASCII art banner for the duplicate device records Microsoft Defender for Endpoint scanner script

Jordan, If you’re reading this…You’re the one who put the idea in my head to slap a proper cowboy hat banner on this thing. Much obliged, partner! #DutchCowboy 🤠

The MDVM exclusion helper? That’s a proof of concept. And I want to be honest about what that means in practice.

Yes, it works. Yes, it will exclude devices from your vulnerability management reports. But ā€œworksā€ comes with a lot of asterisks ****.

You need to manually capture browser cookies, those cookies expire, the APIs are undocumented and could change without notice, and the whole setup requires XDRInternals plus a working understanding of portal authentication. It’s not exactly ā€œrun it and forget itā€ automation. It’s more like ā€œrun it when you need it and hope Microsoft didn’t change the endpoints since last Tuesday.ā€

I built this because the capability to programmatically exclude duplicate device records simply didn’t exist anywhere else, and I wanted to prove it could be done. But I’m one person with a PowerShell window and a stubbornness problem.

I’ll spare you the cookie-capturing tutorial. If you’re brave enough to run scripts against undocumented APIs, I trust you know how to open DevTools and grab a session token.

A Call to the Community

If you’re reading this and thinking ā€œI could make this better,ā€ please do. Seriously. Whether that’s building a more robust authentication flow, wrapping this in a proper module, creating a GUI, or even convincing Microsoft to add exclusion to the public API (looking at you, product team šŸ‘€), I’d love to see it.

The scanner script, the exclusion research, and all the API documentation are on GitHub Fork it, break it, rebuild it, make it better. If this proof of concept inspires someone smarter than me to build something that actually automates the full lifecycle, then this whole project was worth every late night.

The best tools in this community have always been built together. 🄷

Why Duplicate Device Records Pile Up in MDE

Microsoft Defender identifies devices by a generated 40-character hex ID. So far so good. However, every time a device’s hostname changes, gets reimaged, or goes through an offboard/re-onboard cycle, MDE creates a brand new device record. The old one just… sticks around.

MDE does have built-in deduplication, and it’s on by default. However, it doesn’t catch everything. Those missed duplicates clutter up your device inventory, skew your device counts, and make it harder to get an accurate picture of your security posture.

MDE device inventory showing macOS duplicates tagged as StaleOrphan by the duplicate device records Microsoft Defender for Endpoint scanner script

The kicker? There is no way to delete a device record from Defender. And honestly, there’s a good reason for that. If admins could permanently remove devices, so could an attacker who gains a foothold in your environment. Wipe all device records, cover your tracks, and nobody can verify what happened. The forensic trail would be gone. So Microsoft made device records immutable by design. They go inactive after 7 days, drop out of exposure scores after 30 days, and finally disappear after 180 days.

Fair enough. But here’s where it gets painful. For Windows devices, you can at least offboard them via the API to stop telemetry and start the countdown. For macOS, iOS, Android, and Linux? The offboard API endpoint simply doesn’t support those platforms. You can exclude devices from Vulnerability Management in the portal (even in bulk), but there’s no way to import a CSV, or call an API. Nothing ruins your Monday morning coffee quite like manually ticking checkboxes on 47 duplicate device records scattered across pages of inventory. ā˜•

For macOS specifically, there’s an additional challenge. Serial numbers are often empty in Advanced Hunting queries. Therefore, the approach I used for the Entra ID duplicate scanner (serial number as the stable anchor) doesn’t work here. I needed a different identifier.

HardwareUuid: The Stable Anchor in MDE

After digging through the MDE REST API and Advanced Hunting schemas, I found HardwareUuid. This is a unique hardware identifier that stays constant regardless of hostname changes, reimaging, or re-onboarding. It’s the key to reliably grouping duplicate device records in Microsoft Defender for Endpoint, and the equivalent of what serial numbers are for Entra ID: the fingerprint of the physical device.

The catch? HardwareUuid is only available through Advanced Hunting KQL queries. You cannot get it from the REST API alone. Additionally, Advanced Hunting has a 30-day lookback window, so very stale devices may not have HardwareUuid data available. The script handles this gracefully by falling back to a staleness-only analysis for those ā€œunresolvedā€ devices.

For mobile devices (iOS and Android), HardwareUuid is often empty too. In those cases, the script pulls the IMEI from Intune via Microsoft Graph API and uses that as the fallback grouping identifier. Because when the herd is scattered, you use every tool in the saddlebag. 🤠

Here’s the KQL query the script runs under the hood to grab HardwareUuid:

DeviceInfo
| where OSPlatform in ("macOS", "MacOsX")
| summarize arg_max(Timestamp, *) by DeviceId
| extend HardwareUuid = coalesce(
column_ifexists("HardwareUuid", ""),
tostring(parse_json(AdditionalFields).HardwareUuid)
)
| project DeviceId, DeviceName, HardwareUuid, MergedToDeviceId, MergedDeviceIds, Model

The coalesce + column_ifexists pattern handles the fact that some tenants expose HardwareUuid as a direct column while others still have it nested in the AdditionalFields JSON. Because consistency is apparently optional. šŸ˜…

How the Script Finds Duplicate Device Records

The detection pipeline for finding duplicate device records works in multiple stages. Let me walk you through the flow:

Step 1: Authenticate with OAuth 2.0

The script supports both client secret and certificate-based authentication. Certificate auth uses a JWT client assertion with RS256 signing, so no external modules are needed. In guided setup mode, secrets and passwords are read from the clipboard. Nothing appears on screen.

Step 2: Pull All Devices from the REST API

The script calls GET /api/machines with platform filtering and handles pagination. MDE returns up to 10,000 records per page, and the script follows @odata.nextLink until all devices are collected.

Step 3: Run Advanced Hunting for HardwareUuid

A KQL query pulls HardwareUuid, MergedToDeviceId, MergedDeviceIds, and Model data. A second KQL query pulls DeviceLogonEvents for recent user activity per device.

Step 4: Cross-reference Intune (Automatic)

If Microsoft Graph permissions are available, the script automatically pulls Intune managed device data. It matches MDE devices to Intune by AadDeviceId, adding enrollment status, compliance state, last sync date, and IMEI. If Graph permissions aren’t available, this step is skipped gracefully. No errors, no drama.

Step 5: Correlate and Group

REST API data, Advanced Hunting results, logon activity, and Intune data are merged into enriched device records. Devices are then grouped by HardwareUuid (or IMEI for mobile) to identify duplicate groups.

Step 6: Score Each Record

This is where the magic happens. Every record in a duplicate group gets scored across 21 heuristic signals.

Scoring Duplicate Device Records: 21 Signals Across 3 Tiers

I’m a firm believer that ā€œstale = deleteā€ is too simplistic when dealing with duplicate device records. A device might be inactive for 10 days because the user is on vacation. That’s not the same as a device that hasn’t checked in for 120 days, has no sensor data, no Entra registration, no logon activity, and its sibling is actively reporting. The scoring engine evaluates 21 signals organized across three tiers. Overkill again?? Probably…

Tier 1: MDE REST API Signals (Always Active)

These signals come from data that’s always available, no Advanced Hunting or Graph required:

Score Signal
+1 to +4 Graduated inactivity: +1 (8-14 days), +2 (15 to threshold), +3 (threshold to 3x), +4 (beyond 3x threshold)
+3 MergedToDeviceId present: MDE already absorbed this record into another
+3 Confirmed ghost: group leader active within 7 days, this record has >7 day gap
+2 OnboardingStatus = InsufficientInfo
+2 HealthStatus = NoSensorData or ImpairedCommunication
+2 No AadDeviceId: no Entra registration link
+1 Older firstSeen: not the newest record in the group
+1 No tags while sibling has tags
+1 Same AadDeviceId as group leader: ghost shares Entra identity
+1 Oldest agentVersion in group: stale sensor build
+1 DefenderAvStatus = notUpdated or disabled
-2 Has MergedDeviceIds: this is the survivor record
-1 Health: Active
-1 Most recent lastSeen in group
-1 Newest agentVersion in group

The graduated inactivity scoring is worth calling out. In v1.0, any device inactive for more than 7 days scored +3 immediately. That caught too many false positives. Someone goes on a two-week holiday and suddenly their Mac gets flagged as an orphan? Not cool. The graduated approach gives lower scores for shorter absences and reserves the highest penalty for truly abandoned devices.

Tier 2: Advanced Hunting Logon Activity (Automatic)

Score Signal
+2 No logon events in last 30 days while sibling has logon activity
-1 Has recent logon activity

This is powerful. A device might still appear ā€œactiveā€ in MDE health status, but if nobody has actually logged on to it in 30 days while its sibling has active logon events, that’s a strong signal.

Tier 3: Intune Cross-Reference (Automatic, Opt-Out with -SkipIntune)

Score Signal
+2 No Intune enrollment while sibling is enrolled
+1 Intune enrolled but non-compliant
-2 Active Intune enrollment with recent sync (within 30 days)

Intune data adds the MDM layer. A device that’s enrolled, compliant, and recently synced gets a significant negative score (protection from being flagged). A device with no Intune enrollment at all while its sibling is enrolled? That’s a strong orphan signal.

How Scores Become Recommendations

After scoring, each device gets a recommendation:

  • TAG (High confidence): OrphanScore >= 5. Safe to tag and exclude.
  • REVIEW (Moderate): OrphanScore >= 3. Probably an orphan, but human eyes recommended.
  • REVIEW (Low): OrphanScore < 3. Low confidence, proceed with caution.
  • KEEP (Primary): Lowest score in the group. This is your real device.

CSV report with OrphanScore and recommendations generated by the duplicate device records Microsoft Defender for Endpoint scanner script

Pro tip: The score range across all 21 signals is -8 to +27. A device scoring 12+ is about as dead as a doornail. A device scoring 2? That’s probably just someone on PTO. Trust the scoring but always review the CSV!

Duplicate Device Records Scanner Output

The scanner generates several CSV reports:

File Contents
all_mde_{platform}_devices.csv All devices with full analysis detail
duplicate_{platform}_records.csv Only duplicate groups (2+ records per HardwareUuid)
tag_{platform}_recommendations.csv Flagged records with scores, reasons, and exclusion advice
unresolved_{platform}_devices.csv Devices without HardwareUuid (staleness-only analysis)
Invoke-ExcludeDevices.ps1 Generated exclusion helper (when -GenerateExclusionScript is used)
scan_transcript.log Full session transcript for audit trail

Two Modes for Handling Duplicate Device Records

The script has two modes of operation for handling duplicate device records, and this distinction is important.

The Manual Way (Portal UI)

Before we get into automation, let’s look at what Microsoft actually supports today. If you want to exclude a device from Vulnerability Management, here’s the official process:

  1. Go to security.microsoft.com → Assets → Devices
  2. Find the device you want to exclude and click on it
  3. Select the three dots menu (…) and choose Exclude device
  4. Pick a justification from the dropdown (compromised, test/lab, or other)
  5. Add optional notes and confirm

Manual exclude device dialog in Defender portal for removing duplicate device records Microsoft Defender for Endpoint scanner script identifies

To be fair, you can select multiple devices in the inventory and bulk-exclude them from the actions bar. So it’s not strictly one at a time. But there’s no CSV import and no API call. For a handful of devices, the portal works fine. For fifty scattered across pages of inventory? You’re still manually ticking checkboxes. For hundreds? That’s when you start looking at the automation options below. ā˜• For the full walkthrough, check the official Microsoft documentation.

Mode 1: Scan and Tag (Safe, Read-Write via Official API)

# Scan and tag high-confidence orphans
./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" -AppSecret "<secret>" `
-TagStaleDevices

This mode uses the official MDE REST API to tag duplicate device records with a configurable tag (default: StaleOrphan_macOS, StaleOrphan_Windows, etc.). Tagging is non-destructive. It adds a label to the device in the portal that you can filter on. Nothing gets deleted, excluded, or offboarded. You can even run it with -WhatIf first to see what would be tagged without making any changes.

This is the ā€œsleep well at nightā€ option. 😓

About API tags… MDE supports multiple ways to tag devices: manually through the portal, via the REST API, through a registry key (HKLM\...\DeviceTagging\Group) deployed by GPO or SCCM, via an Intune custom OMA-URI profile, through config profiles for macOS and Linux, or dynamically using Asset Rule Management in Defender XDR.

The script uses the API method (POST /api/machines/{id}/tags) because it writes the tag server-side directly on the cloud record. That matters here because stale and orphaned devices aren’t checking in anymore. They would never pick up a registry change, an Intune profile, or a config profile update. The API is the only method that can reach devices that have already gone dark.

Mode 2: Scan, Tag, and Generate Exclusion Script (Uses Undocumented APIs)

# Scan, tag, AND generate the exclusion helper
./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" -AppSecret "<secret>" `
-TagStaleDevices -GenerateExclusionScript

When you add the -GenerateExclusionScript flag, the scanner generates a self-contained helper script called Invoke-ExcludeDevices.ps1. This helper script can auto bulk-exclude flagged devices from Microsoft Defender Vulnerability Management (MDVM) by calling undocumented portal APIs.

This is where things get spicy. šŸŒ¶ļø

Excluding Duplicate Device Records: The Undocumented API Rabbit Hole

Here’s the thing. Microsoft provides no official REST API or Graph API endpoint for excluding duplicate device records from Vulnerability Management. As of February 2026, the only supported method is clicking through the UI at security.microsoft.com. For organizations managing hundreds or thousands of devices, that’s not exactly scalable.

So I did what any self-respecting cowboy would do. I opened the browser DevTools, clicked the ā€œExcludeā€ button in the portal, and captured the network request. Two API endpoints emerged from the browser traffic:

POST /apiproxy/mtp/k8s/machines/UpdateExclusionState
GET /apiproxy/mtp/ndr/machines/{id}/exclusionDetails

The POST endpoint accepts a JSON body with the device IDs, exclusion state, justification, and notes. The GET endpoint returns the current exclusion details for a device.

Scripted exclude device dialog in Defender portal for removing duplicate device records Microsoft Defender for Endpoint scanner script identifies

The Authentication Challenge

So why can’t we just reuse the same OAuth token from the main scanner? It comes down to how Microsoft architected these two surfaces.

The official MDEAPI (api.security.microsoft.com) is a public REST API designed for programmatic access. You register an app in Entra ID, grant it permissions, and authenticate with OAuth 2.0 client credentials. Standard stuff.

The exclusion endpoints (security.microsoft.com/apiproxy/mtp/…) are a completely different story. Microsoft provides no public API for device exclusion. The only documented method is through the portal UI. When you click the ā€œExcludeā€ button in the Defender portal, your browser calls these internal endpoints behind the scenes. They were never designed for programmatic access. They expect a logged-in user’s browser session, complete with:

  • sccauth cookie (portal session token)
  • XSRF-TOKEN cookie + x-xsrf-token header (CSRF protection)

In other words, these APIs only trust ā€œI’m a human clicking buttons in a browser,ā€ not ā€œI’m a service principal with app permissions.ā€ That’s why we need a completely separate authentication flow, and why XDRInternals exists: it bridges the gap between scripting and the portal by replaying browser session cookies programmatically.

So What Are These Cookies, Exactly?

After spending way too many hours reading about browser cookies and authentication flows, here’s how I think this works in practice:

It’s essentially a two-step trust chain:

  • ESTSAUTHPERSISTENT is your proof that you have a valid Entra ID browser session. You’ve passed MFA, Conditional Access, all the checks. Microsoft documents these as browser session cookies used for SSO.
  • sccauth is your proof that you have a valid Defender portal session tied to a specific tenant. Microsoft doesn’t publicly document this one (of course), but it’s what makes the portal’s internal API endpoints trust your requests.

XDRInternals bridges these two worlds. Its Connect-XdrByEstsCookie cmdlet takes your ESTSAUTHPERSISTENT cookie, hits the security portal’s sign-in flow, and exchanges that Entra session for portal session cookies. That’s the ā€œOAuth redirect danceā€ happening under the hood.

This is the part where I lost track of time and my coffee went cold; On top of the session cookies, the portal also uses something called a ā€œDouble Submit Cookieā€ pattern for CSRF protection. Basically: the server sets an XSRF-TOKEN cookie when you log in, and every write request (POST, PUT, PATCH) must send that same value back in an x-xsrf-token header. If the header is missing or doesn’t match the cookie, you get a 400 Bad Request.

While I was completely geeking out over the portal traffic in DevTools, I stumbled onto something that will save you a few hours of debugging: the XSRF-TOKEN cookie value is URL-encoded, but the x-xsrf-token header expects the decoded version. That means you need a [System.Web.HttpUtility]::UrlDecode() step in your script. Skip it, and you’ll get a cryptic ‘incorrectly loaded page’ error even though your session cookies are perfectly valid. Ask me how I know. šŸ˜…

Credits: Standing on the Shoulders of Giants

I want to give major credit to Fabian Bader and Nathan McNulty for building XDRInternals. Without their work, the exclusion helper for duplicate device records wouldn’t exist.

XDRInternals is a community-driven PowerShell module that provides direct access to the Microsoft Defender XDR portal APIs. It solves the authentication problem by taking an ESTSAUTHPERSISTENT cookie from login.microsoftonline.com, performing the OAuth redirect dance to the security portal, and retrieving the session cookies needed for portal API calls. It even auto-refreshes the XSRF token every 5 minutes.

I actually saw Fabian present XDRInternals live at Workplace Ninja Connect 2026 in the Netherlands just a few weeks ago (February 4, 2026, at the Van der Valk Hotel in Gorinchem). Seeing the module in action during his session is what pushed me over the edge from ā€œI should probably build that exclusion thingā€ to ā€œI’m building it this weekend.ā€ The community at Workplace Ninja events is something special. If you’ve never attended, you’re missing out. 🄷

The XDRInternals module also includes a browser extension called XDRay (available for Edge, Chrome and Firefox) that can automatically capture portal API calls and translate them to Invoke-XdrRestMethod calls. Psssst…This is actually the easiest way to get the sccauth and XSRF tokens for the exclusion helper script.

How the Exclusion Helper Uses XDRInternals

The exclusion helper doesn’t talk to the Defender portal directly. It uses XDRInternals as the authentication layer between your browser session and the undocumented portal APIs. In short: you provide a session cookie, XDRInternals establishes a valid portal connection, and the script uses that connection to call the internal exclusion endpoints (/apiproxy/mtp/k8s/machines/UpdateExclusionState and /apiproxy/mtp/ndr/machines/{id}/exclusionDetails).

The script offers two authentication methods:

  1. ESTSAUTHPERSISTENT cookie: Captured from the browser login flow. XDRInternals handles the rest.
  2. sccauth + XSRF tokens (default): Captured directly from the portal using DevTools or the XDRay extension.

I spent a lot of time trying to get the ESTSAUTHPERSISTENT flow working reliably, and I kept running into issues. The cookie-to-portal-session exchange is sensitive to timing, Conditional Access policies, and session state in ways that were hard to debug consistently. In the end, I fell back to capturing sccauth + XSRF tokens directly from the portal, which just works. That’s why it’s the default method. If you have better luck with ESTSAUTHPERSISTENT, more power to you. Let me know what I’m doing wrong. šŸ˜… (Fabian??, Nathan??)

Both methods read cookie values from the clipboard with no-echo keypress confirmation. Nothing gets displayed on screen.

āš ļø Why all this paranoia around cookie handling? While researching this, I came across the ā€œCookie-Biteā€ attack, and it really drove the point home. These session cookies are essentially MFA-equivalent tokens. If someone gets hold of your sccauth or ESTSAUTHPERSISTENT cookie, they can walk right into the Defender portal as you, completely bypassing password and MFA. That’s why the exclusion helper never displays cookie values on screen and reads them from the clipboard with masked input. Treat these cookies like passwords. Actually, treat them better than passwords, because they skip the entire login process.

Requirements and Setup

For the Main Scanner

  • PowerShell 7+
  • An Azure App Registration with WindowsDefenderATP permissions:
    • Machine.Read.All (read device records)
    • AdvancedQuery.Read.All (Advanced Hunting for HardwareUuid + logon activity)
    • Machine.ReadWrite.All (optional, for tagging)
  • Microsoft Graph permission (for Intune cross-reference, automatic):
    • DeviceManagementManagedDevices.Read.All
  • Microsoft Defender for Endpoint P1 or P2 license

Automated App Registration Setup

Don’t want to create the app registration manually? Use the included helper script:

# Create app registration with all permissions
./New-MdeAppRegistration.ps1 -IncludeWritePermission -IncludeIntunePermission

This automates the entire setup process: creates the app, adds the required API permissions, generates a client secret, and grants admin consent. Save the AppSecret immediately, because you won’t be able to retrieve it again.

MDE Duplicate Device scanner App Registration

For the Exclusion Helper (Optional)

  • XDRInternals module (Install-Module XDRInternals -Scope CurrentUser)
  • Access to the security.microsoft.com portal
  • A browser to capture session cookies

Running the Duplicate Device Records Scanner

Interactive Guided Setup

If you run the script with no parameters at all, it walks you through everything interactively. Platform selection, authentication method, Intune cross-reference, tagging, exclusion script generation. Everything.

./Find-DuplicateDefenderDevices.ps1

Prerequisites and app registration steps for the duplicate device records Microsoft Defender for Endpoint scanner script

Scan complete output with next steps from the duplicate device records Microsoft Defender for Endpoint scanner script

Now confirm the devices have been tagged and check the CSV files. If you’d rather grab the reins yourself…

Basic Scan (macOS, Default)

./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" -AppSecret "<secret>"

Scan Windows Devices

./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" -AppSecret "<secret>" `
-Platform Windows

Scan All Platforms

./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" -AppSecret "<secret>" `
-Platform All

Certificate Authentication

./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" `
-CertificatePath "cert.pfx" -CertificatePassword "pass"

Full Workflow: Scan, Tag, and Generate Exclusion Script

./Find-DuplicateDefenderDevices.ps1 `
-TenantId "<id>" -AppId "<id>" -AppSecret "<secret>" `
-TagStaleDevices -GenerateExclusionScript

Run the Generated Exclusion Script

Once the scanner finishes with -GenerateExclusionScript, it drops an Invoke-ExcludeDevices.ps1 file right next to your CSV reports. This is the second half of the workflow: taking those tagged, high-confidence orphans and excluding them from Vulnerability Management.

Before you run it, you’ll need one thing the scanner didn’t need: a browser session. Remember, this script talks to undocumented portal APIs, not the official REST API. That means it authenticates through your browser’s session cookies using XDRInternals, not through an app registration.

# Exclude devices with OrphanScore >= 5 (default)
./Invoke-ExcludeDevices.ps1

The first thing you’ll see when you run it is a wall of warnings. And I mean a wall. That’s intentional…

This script talks to undocumented portal APIs, not the official REST API. It requires browser session cookies for authentication and comes with zero guarantees from Microsoft. You have to type YES in full to continue. No fat-fingering your way past this one.

Disclaimer and warning when running the exclusion helper from the duplicate device records Microsoft Defender for Endpoint scanner script

After accepting the risk, the script loads the CSV generated by the scanner and filters devices based on their OrphanScore. By default, it only picks up high-confidence orphans (score >= 5). You’ll see exactly which devices were selected before anything happens.

Next up: authentication. The script gives you two options:

  1. ESTSAUTHPERSISTENT cookie (recommended by XDRInternals): Your Entra ID browser session cookie. The module uses this to perform the OAuth redirect dance and obtain portal access.
  2. Manual sccauth + XSRF tokens: If the ESTS cookie method gives you trouble, you can grab the sccauth and XSRF-TOKEN values directly from your browser’s DevTools or the XDRay extension.

Authentication method selection after the duplicate device records Microsoft Defender for Endpoint scanner script loads flagged devices

I used option 2: sccauth + XSRF tokens.. Go grab the sccauth and XSRF-TOKEN

Once authenticated, the script runs a pre-flight check on every device: are they already excluded? Still reachable? It shows you a summary with the justification and notes, then asks for explicit confirmation before proceeding. Nothing happens without your green light.

Exclusion report showing four devices successfully excluded by the duplicate device records Microsoft Defender for Endpoint scanner script

After exclusion completes, you get a detailed report showing exactly what happened to each device. The script also reminds you that it can take up to 10 hours for exclusions to fully reflect in MDVM views. And notice that last line: “Close this PowerShell window for maximum security.” Those session cookies are powerful. Treat them accordingly.

The -Action GetStatus option is your “trust but verify” move. Run it after excluding to confirm the portal actually processed your requests. Because with undocumented APIs, you always double-check.

# Check exclusion status without making changes
./Invoke-ExcludeDevices.ps1 -Action GetStatus

Security and Credential Handling

Security matters. Especially when you’re building tools that interact with security APIs to manage duplicate device records. Here’s what the script does to protect your credentials:

Main scanner: – Client secrets and .pfx passwords are read from the clipboard in guided setup mode. Nothing appears on screen. – The finally block scrubs AppSecret, CertificatePassword, OAuth tokens, and Graph tokens from memory on exit (normal or interrupted). – Transcript logging starts after authentication so no sensitive data gets logged.

Exclusion helper: – Cookie values are read via clipboard with no-echo keypress confirmation. – On exit, the script clears cookie variables, clipboard contents, PSReadLine command history, and forces garbage collection. – A runtime risk acceptance gate (type ā€˜YES’ to proceed) ensures you know what you’re about to do.

Disclaimers and Warnings

Alright, serious hat on for a moment. šŸ¤ āž”ļøšŸŽ©

The main scanner (Find-DuplicateDefenderDevices.ps1) uses official, documented MDE REST APIs and Advanced Hunting to find duplicate device records. Scanning is read-only by default. The -TagStaleDevices switch adds tags but does NOT delete or offboard devices. This is as safe as it gets.

The exclusion helper (Invoke-ExcludeDevices.ps1) uses undocumented portal APIs as covered earlier in this post. I can’t stress this enough: test in a controlled, non-production environment before deploying to production!

And please, don’t just trust any script you find out there (including mine). Read the code. Understand what it does. Review the CSV reports before taking action. The script provides recommendations, but the final decision is always yours.

I’m just a guy with a terminal and too much coffee. No guarantees, no warranties, no SLA. The code is open. Read it, review it, and decide for yourself if it meets your bar. This script is provided ā€œAS ISā€, without warranty of any kind, express or implied. You are solely responsible for any actions taken using this script. USE AT YOUR OWN RISK.Ā 

What’s Next?

The 180-day retention cycle is still the only way to truly remove duplicate device records from MDE. In a perfect world, Microsoft would give us an official API for device exclusion. Until then, this script helps you take back control of your device inventory.

If you find issues, have feature requests, or want to contribute, the script is on GitHub. Pull requests are welcome.

And if you’re attending any upcoming community events, come say hi. I’ll be the one in the cowboy hat. 🤠

Resources

0 0 votes
Article Rating

Oktay Sari

Boots on the Ground, Head in the Cloud - Just a digital Cowboy, Ridin' the Rains of Change! šŸ¤ šŸ’Ø

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments