Deploy MDE Toolkit across your fleet with centralized telemetry collection, Azure Function App ingestion, and Power BI-ready Table Storage.
The MDE Toolkit enterprise collector runs silently on each endpoint, collecting security telemetry and uploading it to a central Azure Function App for storage in Azure Table Storage.
┌──────────────────────────────────────┐ │ Endpoint (Windows PC) │ │ │ │ ┌────────────────────────────────┐ │ │ │ Scheduled Task: Collect │ │ HTTPS/Bearer ┌──────────────────────┐ Table API ┌─────────────────────┐ │ │ Runs as: SYSTEM │ │ │ Azure Function App │ │ Azure Table │ │ │ Every 8 hours │ │ │ (PowerShell, MI) │ │ Storage │ │ │ --collect --no-upload │──┼── writes to cache ──┐ │ │ │ │ │ └────────────────────────────────┘ │ │ │ • Validates JSON │ │ • HealthReports │ │ │ │ │ • Flattens fields │ │ • One row/device │ │ ┌────────────────────────────────┐ │ │ │ • Writes to Table │ │ • Power BI ready │ │ │ Scheduled Task: Upload │ │ reads cache ───────┘ │ │ │ │ │ │ Runs as: Interactive User │ │ ────────────────────► │ │ ──────────────────► │ │ │ │ (BUILTIN\Users group) │ │ POST /api/ingest │ │ Upsert entity │ │ │ │ Every 8 hours │ │ + Entra bearer token │ │ │ │ │ │ --upload │ │ │ │ │ │ │ └────────────────────────────────┘ │ └──────────────────────┘ └─────────────────────┘ │ │ │ │ │ Registry config │ │ Managed Identity │ Power BI │ HKLM\SOFTWARE\ │ │ (Storage Table │ or custom │ Policies\MDE-Toolkit │ │ Data Contributor) │ dashboard └──────────────────────────────────────┘
DefaultAzureCredential can use to authenticate to Azure. Splitting collect and upload gives you the best of both worlds.
# Create resource group
az group create --name rg-mde-toolkit --location eastus
# Create storage account
az storage account create \
--name stmdetoolkit \
--resource-group rg-mde-toolkit \
--sku Standard_LRS \
--kind StorageV2
# Create the table (optional — the Function App auto-creates it)
az storage table create \
--name HealthReports \
--account-name stmdetoolkit
# Create Function App (PowerShell 7.4 runtime)
az functionapp create \
--name func-mde-toolkit \
--resource-group rg-mde-toolkit \
--storage-account stmdetoolkit \
--consumption-plan-location eastus \
--runtime powershell \
--runtime-version 7.4 \
--functions-version 4 \
--os-type Windows
# Enable system-assigned Managed Identity
az functionapp identity assign \
--name func-mde-toolkit \
--resource-group rg-mde-toolkit
# Set app settings
az functionapp config appsettings set \
--name func-mde-toolkit \
--resource-group rg-mde-toolkit \
--settings \
StorageAccountName=stmdetoolkit \
StorageTableName=HealthReports \
AzureEnvironment=Public
The Function App's Managed Identity needs Storage Table Data Contributor on the storage account:
# Get the Function App's principal ID
PRINCIPAL_ID=$(az functionapp identity show \
--name func-mde-toolkit \
--resource-group rg-mde-toolkit \
--query principalId -o tsv)
# Get the storage account resource ID
STORAGE_ID=$(az storage account show \
--name stmdetoolkit \
--resource-group rg-mde-toolkit \
--query id -o tsv)
# Assign Storage Table Data Contributor
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Storage Table Data Contributor" \
--scope $STORAGE_ID
Create an App Registration so endpoints can acquire a bearer token scoped to the Function App:
# Create app registration
az ad app create \
--display-name "MDE Toolkit Function" \
--identifier-uris "api://mde-toolkit-func" \
--sign-in-audience AzureADMyOrg
# Note the Application (client) ID — this becomes the FunctionAppAudience
# registry value: api://mde-toolkit-func
The Function App is a single PowerShell HTTP trigger that receives the JSON snapshot from each endpoint, flattens it into table-friendly columns, and upserts it into Azure Table Storage.
FunctionApp/
├── host.json
├── requirements.psd1
├── profile.ps1
└── HealthReportIngestion/
├── function.json # HTTP POST trigger binding
└── run.ps1 # Ingestion logic
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "Request",
"methods": ["post"]
},
{
"type": "http",
"direction": "out",
"name": "Response"
}
]
}
The PowerShell script performs these steps on each incoming request:
https://storage.azure.comEvery row in the HealthReports table contains these categories of flat columns, plus the full JSON in ReportJson:
| Category | Columns |
|---|---|
| Identity | Hostname, OsVersion, OrganizationId, DeviceGroup |
| Defender AV | DefenderEnabled, RealTimeProtectionEnabled, BehaviorMonitorEnabled, TamperProtectionEnabled, SignatureAgeHours, DefenderAvStatus |
| MDE / Sense | IsOnboarded, SenseIsRunning, MdeStatus |
| ASR | AsrIsConfigured, AsrTotalRules, AsrBlockModeRules, AsrAuditModeRules, AsrStatus |
| Firewall | FirewallDomainEnabled, FirewallPrivateEnabled, FirewallPublicEnabled, FirewallStatus |
| Network Protection | NetworkProtectionMode, NetworkProtectionStatus |
| Device Control | DeviceControlEnabled, DeviceControlRuleCount, DeviceControlStatus |
| App Control | AppControlIsConfigured, AppControlIsEnabled, AppControlStatus |
| VBS / HVCI | VbsEnabled, VbsStatus, HvciConfigured, HvciRunning, CredentialGuardConfigured, CredentialGuardRunning |
| Entra ID | IsAzureAdJoined, IsHybridJoined |
| Health | HealthScore (0-100), OverallStatus, CriticalEventCount |
| Full Report | ReportJson — complete snapshot for drill-down in Power BI |
# From the repo root, zip and deploy the Function App
cd FunctionApp
func azure functionapp publish func-mde-toolkit --powershell
For Azure Government, set AzureEnvironment=Government in app settings. The script auto-detects the storage endpoint suffix (core.usgovcloudapi.net) and uses the correct OAuth resource URI.
All MDE Toolkit enterprise settings are read from a single registry key. Deploy these via GPO Preferences, Intune Settings Catalog (custom OMA-URI), SCCM, or any registry management tool.
HKLM\SOFTWARE\Policies\MDE-Toolkit
| Value Name | Type | Default | Description |
|---|---|---|---|
EnableBackgroundCollector | DWORD | 0 | 1 = enable background collection, 0 = off |
CollectionIntervalMinutes | DWORD | 480 | Minutes between collections (minimum 5, default 8 hours) |
CacheFolder | SZ | %ProgramData%\MDE-Toolkit\cache | Local folder for JSON snapshot cache |
MaxCacheFiles | DWORD | 48 | Rolling file count — oldest deleted first |
CacheRetentionDays | DWORD | 30 | Delete files older than N days (0 = no age limit) |
| Value Name | Type | Default | Description |
|---|---|---|---|
EnableUpload | DWORD | 0 | 1 = enable upload to Function App |
FunctionAppUrl | SZ | Full URL, e.g. https://func-mde-toolkit.azurewebsites.net/api/HealthReportIngestion | |
FunctionAppAudience | SZ | Entra app audience, e.g. api://mde-toolkit-func | |
UploadIntervalMinutes | DWORD | 480 | How often to upload (minimum 5, default 8 hours) |
UploadTimeoutSeconds | DWORD | 30 | HTTP POST timeout (5–300) |
UploadRetryCount | DWORD | 2 | Retries on transient failure (0–10) |
| Value Name | Type | Default | Description |
|---|---|---|---|
EnableBlobUpload | DWORD | 0 | 1 = enable direct blob upload |
BlobStorageAccountName | SZ | Storage account name | |
BlobContainerName | SZ | mde-telemetry | Blob container name |
BlobEndpointSuffix | SZ | e.g. blob.core.usgovcloudapi.net for Gov Cloud |
| Value Name | Type | Default | Description |
|---|---|---|---|
TenantId | SZ | (auto) | Entra tenant ID — auto-detected from PRT if blank |
PreferredAuthMethod | SZ | Default | Default | WAM | DeviceCode | ManagedIdentity |
AzureEnvironment | SZ | Public | Public | USGov | China | Germany |
AuthorityHostOverride | SZ | Custom authority URI for sovereign/air-gapped clouds |
Control which telemetry categories are collected. All DWORD, 1 = on, 0 = off.
| Value Name | Default | Description |
|---|---|---|
CollectDefenderStatus | 1 | MpComputerStatus — RTP, Tamper, Signatures, etc. |
CollectDefenderPolicies | 1 | Policy Manager — ASR, NP, CFA, Device Control |
CollectFirewallStatus | 1 | Firewall profile status (Domain/Private/Public) |
CollectFirewallRules | 0 | Individual firewall rules (can be large) |
CollectWfpFilters | 0 | WFP filter summaries |
CollectDeviceControl | 1 | Device Control policies and groups |
CollectAppControl | 1 | WDAC / App Control status |
CollectIntuneEnrollment | 1 | Intune / ConfigMgr enrollment info |
CollectDeviceGuard | 1 | VBS, HVCI, Credential Guard status |
CollectNetworkInfo | 0 | Network adapter and IP information |
CollectSecurityScore | 0 | Computed security score |
| Value Name | Type | Description |
|---|---|---|
DeviceTag | SZ | Freeform label — e.g. Finance, LOB1 |
OrgUnit | SZ | Organizational unit — e.g. US-East |
Environment | SZ | Production | Staging | Dev |
These control the interactive WPF application, not the background collector. All DWORD, default 0.
| Value Name | Description |
|---|---|
DisableAiFeatures | Hide AI analysis panels |
DisableExportButtons | Hide PDF/JSON export |
DisableAdvancedNetworking | Hide advanced networking window |
DisableRemoteTarget | Prevent targeting remote machines |
ForceReadOnlyMode | Disable all write operations (remediation, policy push) |
| Value Name | Type | Default | Description |
|---|---|---|---|
LogLevel | SZ | Info | None | Error | Warning | Info | Verbose |
LogFilePath | SZ | cache\service.log | Path to service log file |
MaxLogSizeMb | DWORD | 10 | Max log size before rotation (1–500 MB) |
$regPath = "HKLM:\SOFTWARE\Policies\MDE-Toolkit"
New-Item -Path $regPath -Force | Out-Null
# Enable collector + upload
Set-ItemProperty $regPath -Name EnableBackgroundCollector -Value 1 -Type DWord
Set-ItemProperty $regPath -Name EnableUpload -Value 1 -Type DWord
# Function App config
Set-ItemProperty $regPath -Name FunctionAppUrl -Value "https://func-mde-toolkit.azurewebsites.net/api/HealthReportIngestion"
Set-ItemProperty $regPath -Name FunctionAppAudience -Value "api://mde-toolkit-func"
# Cloud environment (Public, USGov, China, Germany)
Set-ItemProperty $regPath -Name AzureEnvironment -Value "Public"
# Tagging
Set-ItemProperty $regPath -Name DeviceTag -Value "Finance"
Set-ItemProperty $regPath -Name OrgUnit -Value "US-East"
# Cache retention
Set-ItemProperty $regPath -Name CacheRetentionDays -Value 30 -Type DWord
The MDE Toolkit ships as a standard WiX v5 MSI installer, suitable for deployment via SCCM, Intune, or GPO.
C:\Program Files\MDE-Toolkit\ (self-contained .NET 8)C:\ProgramData\MDE-Toolkit\cache\ with SYSTEM + Administrators ACLHKLM\SOFTWARE\MDE-Toolkit with Version and InstallPath--collect --no-upload)--upload)# Silent install
msiexec /i MDE-Toolkit-3.0.2.msi /qn
# Silent install with logging
msiexec /i MDE-Toolkit-3.0.2.msi /qn /l*v install.log
# Intune detection rule: HKLM\SOFTWARE\MDE-Toolkit\Version exists
The MSI installer creates two scheduled tasks automatically. Collection and upload are split to leverage the right security context for each operation.
Runs as SYSTEM every 8 hours. SYSTEM has full access to read all Defender configuration, firewall rules, WFP filters, and registry settings — but cannot acquire a user Entra ID token.
schtasks /create /tn "MDE Toolkit Collect" ^
/tr "\"C:\Program Files\MDE-Toolkit\MDE Monitoring App.exe\" --collect --no-upload" ^
/sc HOURLY /mo 8 ^
/ru SYSTEM ^
/rl HIGHEST ^
/f
The --collect --no-upload flags tell the app to:
C:\ProgramData\MDE-Toolkit\cache\latest.jsonRuns as the currently logged-on user every 8 hours. The user's Entra ID Primary Refresh Token (PRT) allows DefaultAzureCredential to acquire a bearer token for the Function App — no stored passwords or secrets needed.
# The MSI uses Register-ScheduledTask with BUILTIN\Users group principal
# so the task runs as whichever user is interactively logged on:
$action = New-ScheduledTaskAction `
-Execute '"C:\Program Files\MDE-Toolkit\MDE Monitoring App.exe"' `
-Argument '--upload'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Hours 8)
$principal = New-ScheduledTaskPrincipal `
-GroupId 'BUILTIN\Users' -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName 'MDE Toolkit Upload' `
-Action $action -Trigger $trigger -Principal $principal `
-Settings $settings -Force
The --upload flag tells the app to:
latest.json--collect and --upload modes run as headless console processes. The WPF window is never created.
BUILTIN\Users with interactive logon, so it only fires when a user session is active. If no user is logged on, the upload is deferred until the next trigger with StartWhenAvailable.
# Run a single collect cycle (as admin, simulating the SYSTEM task)
& "C:\Program Files\MDE-Toolkit\MDE Monitoring App.exe" --collect --no-upload
# Check the cached snapshot
Get-Content "C:\ProgramData\MDE-Toolkit\cache\latest.json" | ConvertFrom-Json | Select hostname, healthScore, overallStatus
# Check the service log
Get-Content "C:\ProgramData\MDE-Toolkit\cache\service.log" -Tail 20
# Run a single upload cycle (uses your Entra PRT for auth)
& "C:\Program Files\MDE-Toolkit\MDE Monitoring App.exe" --upload
# Check the service log for upload result
Get-Content "C:\ProgramData\MDE-Toolkit\cache\service.log" -Tail 10
# Both tasks should exist after MSI install
Get-ScheduledTask -TaskName "MDE Toolkit*" | Format-Table TaskName, State, @{N='RunAs';E={$_.Principal.GroupId ?? $_.Principal.UserId}}
# Query the table for your machine
az storage entity query \
--table-name HealthReports \
--account-name stmdetoolkit \
--filter "Hostname eq 'YOUR-PC-NAME'"
| Check | Expected |
|---|---|
| Exit code | 0 |
latest.json exists | Yes, with hostname, summary, defenderPolicies |
| Service log | Function App upload succeeded (200) |
| Table row | RowKey = HOSTNAME_guid, HealthScore > 0 |
| Registry detection | HKLM\SOFTWARE\MDE-Toolkit\Version = 3.0.2 |
Connect Power BI directly to the Azure Table Storage to build fleet-wide security dashboards.
stmdetoolkitHealthReports tableContent column to access all flat fieldsGauge chart on HealthScore with card count by OverallStatus
Bar chart: AsrBlockModeRules vs AsrAuditModeRules per device
Matrix of Hostname × FirewallDomain/Private/PublicEnabled
Pie chart: devices with HvciRunning=true vs false
Slicer on DeviceTag / OrgUnit for filtering
Table of Hostname, GeneratedAtUtc sorted by staleness