Automated device lifecycle management after Intune wipe (factory reset) actions.
Monitors Microsoft Intune for completed device wipe operations and orchestrates the cleanup of device objects from Autopilot, Active Directory, SCCM, and Intune — ensuring no device is removed until the wipe is confirmed as done. Sends Teams notifications for approval requests.
📐 Mermaid diagrams and sequence flow: docs/architecture.md
- Polling — A background service queries the Microsoft Graph API every minute (configurable):
GET /beta/deviceManagement/remoteActionAudits?$filter=action eq 'factoryReset' - Detection — Devices with
actionState == "done"are flagged as ready for cleanup - Notification — A Teams Adaptive Card is sent to the configured channel with device details and an "Approve" button
- Approval — An operator reviews and approves the cleanup from the Blazor dashboard (Entra ID auth required)
- SID Cross-Validation — Before any deletion, the agent validates the device identity across all systems:
- Retrieves the computer SID (
objectSid) from Active Directory - Retrieves the device SID from SCCM (
SMS_R_System.SID) - Compares AD SID ↔ SCCM SID to ensure the same physical device
- If SIDs don't match, the cleanup is blocked with a
SidMismatchresult
- Retrieves the computer SID (
- Cleanup — The orchestrator executes 4 steps in sequence:
- ① Autopilot → Graph API delete
windowsAutopilotDeviceIdentities(cloud) - ② Active Directory → LDAP delete via on-prem Gateway Agent (SignalR)
- ③ SCCM → AdminService REST delete via on-prem Gateway Agent (SignalR)
- ④ Intune → Graph API delete
managedDevices(cloud) - Entra ID → Automatically cleaned up after AD sync
- ① Autopilot → Graph API delete
- Result Notification — Teams card with cleanup result (✅ success / ❌ failure)
- Auditing — Every action is tracked with custom Application Insights events, including matched SIDs
| Project | Description |
|---|---|
Intune.WipeMonitor |
ASP.NET Core Blazor Server web app (Azure) |
Intune.WipeMonitor.Agent |
Worker Service / Windows Service (on-premises) |
Intune.WipeMonitor.Shared |
Shared models, contracts, enums, CMTrace formatter |
All cleanup operations emit structured custom events for auditing and monitoring:
| Event Name | When |
|---|---|
DeviceCleanup.Approved |
Operator approves a device cleanup |
DeviceCleanup.Skipped |
Operator skips a device |
DeviceCleanup.AutopilotDeletion |
Autopilot identity deletion attempted |
DeviceCleanup.ADDeletion |
AD object deletion attempted (emitted by both cloud and agent) |
DeviceCleanup.SCCMDeletion |
SCCM device deletion attempted (emitted by both cloud and agent) |
DeviceCleanup.IntuneDeletion |
Intune device deletion attempted |
DeviceCleanup.Completed |
Full cleanup completed successfully |
DeviceCleanup.Failed |
Cleanup failed on at least one target |
Agent.Connected |
On-prem agent connected to the hub |
Agent.Disconnected |
On-prem agent disconnected |
WipePoll.Completed |
Graph API polling cycle completed |
WipePoll.WipeDetected |
New completed wipe detected |
The agent implements a safety chain to prevent accidental deletion of wrong devices:
CleanupCommand (device name + EntraDeviceId)
│
▼
┌─────────────┐ objectSid ┌─────────────┐
│ Active │ ──────────────► │ Compare │
│ Directory │ (LDAP) │ AD SID │
└─────────────┘ │ vs │
│ SCCM SID │──► Match? → Proceed with deletion
┌─────────────┐ SID │ │──► Mismatch? → Block (SidMismatch)
│ SCCM │ ──────────────► │ │
│ AdminSvc │ (REST) └─────────────┘
└─────────────┘
- If AD SID ≠ SCCM SID: deletion is blocked,
StepResult.SidMismatchis returned - The matched SID is included in the
CleanupStepResultfor audit trail - All SID validation results are logged in CMTrace format and tracked in Application Insights
Example KQL queries:
// All cleanup actions in the last 24h
customEvents
| where name startswith "DeviceCleanup"
| where timestamp > ago(24h)
| project timestamp, name,
DeviceName = tostring(customDimensions.DeviceName),
Result = tostring(customDimensions.Result),
Error = tostring(customDimensions.ErrorMessage)
| order by timestamp desc
// Failed deletions
customEvents
| where name in ("DeviceCleanup.ADDeletion", "DeviceCleanup.SCCMDeletion")
| where customDimensions.Result == "Failed"
| project timestamp, name,
DeviceName = tostring(customDimensions.DeviceName),
Error = tostring(customDimensions.ErrorMessage)When a wipe is completed and a device needs approval, an Adaptive Card is sent to a Microsoft Teams channel:
- 📢 Approval request — device name, owner, wipe date, "Open Approvals" button
- ✅/❌ Cleanup result — device name, who approved, success or failure
- In Teams, go to the target channel → ⋯ → Workflows → "Post to a channel when a webhook request is received"
- Copy the generated URL (e.g.
https://prod-xx.westeurope.logic.azure.com:443/workflows/...) - Set
WipeMonitor:TeamsWebhookUrlin App Configuration or App Settings - Optionally set
WipeMonitor:DashboardUrlfor the button link in the card
Note: The classic Incoming Webhook connector is being deprecated. Use the Power Automate Workflows approach. The Adaptive Card payload is identical for both. If no webhook URL is configured, notifications are silently skipped.
Both the web app and the agent write log files in CMTrace format (<![LOG[...]LOG]!>), compatible with:
- CMTrace.exe (SCCM toolkit)
- OneTrace (Configuration Manager)
Logs are written to logs/wipemonitor-web-YYYYMMDD.log and logs/wipemonitor-agent-YYYYMMDD.log with automatic daily rolling and 30-day retention.
| Resource | Purpose |
|---|---|
| App Service (B1 Linux) | Hosts the Blazor web app |
| Key Vault | Stores the Graph API client secret (private endpoint) |
| App Configuration | Stores all application settings with KV references (private endpoint) |
| Application Insights | Telemetry, custom events, and monitoring |
| VNet | Network isolation with two subnets |
| Private Endpoints | Key Vault and App Config accessible only via VNet |
| Managed Identity | Web app authenticates to Key Vault and App Config without secrets |
- An App Registration in Entra ID with:
DeviceManagementManagedDevices.ReadWrite.All(Application permission)DeviceManagementRBAC.Read.All(Application permission)DeviceManagementServiceConfig.ReadWrite.All(Application permission — Autopilot cleanup)
- Client secret stored in Key Vault
- Windows machine with:
- Network access to Active Directory (LDAP)
- Network access to SCCM AdminService
- Outbound HTTPS to Azure (for SignalR + App Insights)
- Domain account with permissions to delete computer objects in AD
- SCCM account with permissions to delete device records
| Key | Description |
|---|---|
WipeMonitor:PollingIntervalMinutes |
Polling interval (default: 60) |
WipeMonitor:RequireApproval |
Require manual approval (default: true) |
WipeMonitor:CleanupIntune |
Also delete from Intune (default: true) |
WipeMonitor:TeamsWebhookUrl |
Teams Incoming Webhook URL (optional) |
WipeMonitor:DashboardUrl |
Dashboard URL for Teams card links |
Graph:TenantId |
Azure AD tenant ID |
Graph:ClientId |
App Registration client ID |
Graph:ClientSecret |
Key Vault reference to the client secret |
{
"ApplicationInsights": {
"ConnectionString": "<connection-string>"
},
"Agent": {
"AgentId": "AGENT-01",
"HubUrl": "https://intune-wipemonitor-app.azurewebsites.net/hub/cleanup",
"HeartbeatIntervalSeconds": 60,
"ActiveDirectory": {
"Server": "dc01.contoso.com",
"SearchBase": "DC=contoso,DC=com",
"Port": 389
},
"Sccm": {
"AdminServiceUrl": "https://sccm-server.contoso.com/AdminService"
}
}
}dotnet build# Web App
cd src/Intune.WipeMonitor
dotnet run
# Agent (separate terminal)
cd src/Intune.WipeMonitor.Agent
dotnet runsc.exe create "Intune.WipeMonitor.Agent" binpath="C:\Agent\Intune.WipeMonitor.Agent.exe"
sc.exe start "Intune.WipeMonitor.Agent"cd src/Intune.WipeMonitor
dotnet publish -c Release -o publish
Compress-Archive -Path publish/* -DestinationPath publish.zip
az webapp deploy --name intune-wipemonitor-app -g IntuneWipeMonitor-RG --src-path publish.zip --type zipMIT
