Add Microsoft .NET Desktop Runtime 10 as a Windows FMA (#50361)

**Related issue:** Resolves #50360

Adds Microsoft .NET Desktop Runtime 10 as a Windows Fleet-maintained
app, from winget `Microsoft.DotNet.DesktopRuntime.10` (10.0.10, WiX burn
bundle, x64).

This closes the gap opened by HandBrake (#50323 / #50352). HandBrake
requires the .NET **Desktop** Runtime and its install script now
hard-fails without it, but Fleet had no FMA that could satisfy that — we
ship `microsoft-dotnet-runtime-8`/`-10`, which are the *base* runtime, a
different package with its own Add/Remove Programs entry. The customer's
ManageEngine catalog also deploys the Desktop Runtime directly.

## Verification

The winget manifest supplies `AppsAndFeaturesEntries`, and I confirmed
each value against the real installer by extracting the burn bundle's
registration data:

```
Microsoft Windows Desktop Runtime 10.0.10 (x64)
Publisher="Microsoft Corporation"
Version="10.0.10.50000"
```

- Installer SHA confirmed against a local download (`e82fc901…84d1`).
- The `DisplayName` carries both version and architecture, so the exists
query uses the same `LIKE 'Microsoft Windows Desktop Runtime 10.%' AND
name LIKE '%(x64)'` shape as the existing base-runtime FMAs.
- The bundle exposes several ProductCodes (the bundle plus its MSI
components), which is exactly the shape the existing uninstall script
already documents and handles.

**`use_display_version_for_patch` is required here.** The registry
`DisplayVersion` is `10.0.10.50000` but the winget package version is
`10.0.10`. Without the flag the patch policy would compare against the
marketing version and mis-order against what osquery reports. The
generated patched query correctly compares against `10.0.10.50000`.

## Reuse rather than duplication

- **Scripts:** this reuses `microsoft_dotnet_runtime_install.ps1` /
`_uninstall.ps1` unchanged. The Desktop Runtime is the same burn bundle
shape, and the uninstaller already resolves the bundle from the injected
`$PACKAGE_ID` with a Package Cache fallback. Those scripts are already
shared by the two base-runtime FMAs, so this follows the existing
pattern rather than adding a near-identical copy.
- **Icon:** reuses the existing `MicrosoftDotnetRuntime` component and
its `.NET` artwork. The burn bundle only carries a 32×32 icon, so
extracting one would have meant shipping a blurry upscale of the same
logo.

The new map key is `"microsoft .net desktop runtime"` (no version). Icon
lookup is a loose *prefix* match — `s === key || s.startsWith(key + "
")` — so one key covers 10 and any future major, mirroring how
`"microsoft .net runtime"` already serves both base-runtime FMAs. It is
longer than that key, and lookup sorts longest-first, so the desktop
runtime cannot be mis-matched to the base runtime icon.

# Checklist for submitter

- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Microsoft .NET Desktop Runtime 10 to the Windows software
catalog.
* Added support for installing and uninstalling the x64 desktop runtime,
including version detection and reboot handling.
* Added a dedicated Microsoft .NET Runtime icon for the software
listing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
kitzy
2026-08-02 22:10:40 -05:00
committed by GitHub
parent b35904ccfa
commit 5fa8a0a9ea
5 changed files with 44 additions and 0 deletions
@@ -0,0 +1,14 @@
{
"name": "Microsoft .NET Desktop Runtime 10",
"slug": "microsoft-dotnet-desktop-runtime-10/windows",
"package_identifier": "Microsoft.DotNet.DesktopRuntime.10",
"unique_identifier": "Microsoft Windows Desktop Runtime",
"exists_query": "SELECT 1 FROM programs WHERE name LIKE 'Microsoft Windows Desktop Runtime 10.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation';",
"installer_arch": "x64",
"installer_type": "exe",
"installer_scope": "",
"use_display_version_for_patch": true,
"install_script_path": "ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_install.ps1",
"uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_uninstall.ps1",
"default_categories": ["Developer tools"]
}
+7
View File
@@ -5398,6 +5398,13 @@
"unique_identifier": "com.microsoft.StorageExplorer",
"description": "Microsoft Azure Storage Explorer is an explorer for Azure Storage."
},
{
"name": "Microsoft .NET Desktop Runtime 10",
"slug": "microsoft-dotnet-desktop-runtime-10/windows",
"platform": "windows",
"unique_identifier": "Microsoft Windows Desktop Runtime",
"description": "Microsoft .NET Desktop Runtime 10 runs desktop apps built on .NET 10, including Windows Forms and WPF apps."
},
{
"name": "Microsoft .NET Runtime 10",
"slug": "microsoft-dotnet-runtime-10/windows",
@@ -0,0 +1,22 @@
{
"versions": [
{
"version": "10.0.10",
"queries": {
"exists": "SELECT 1 FROM programs WHERE name LIKE 'Microsoft Windows Desktop Runtime 10.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation';",
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name LIKE 'Microsoft Windows Desktop Runtime 10.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation' AND version_compare(version, '10.0.10.50000') < 0);"
},
"installer_url": "https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.10/windowsdesktop-runtime-10.0.10-win-x64.exe",
"install_script_ref": "13facdef",
"uninstall_script_ref": "661ee74e",
"sha256": "e82fc901c8f52d716293b2bc0830ce0dd254a06268c457a19e8fc503560a84d1",
"default_categories": [
"Developer tools"
]
}
],
"refs": {
"13facdef": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n#\n# The .NET Runtime ships as a WiX \"burn\" bootstrapper (.exe). It installs\n# machine-wide and registers its own ARP entry. Silent switches come from the\n# winget installer manifest (Silent: /quiet, Custom: /norestart).\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n if (-not (Test-Path $exeFilePath)) {\n Write-Host \"Error: Installer file not found at: $exeFilePath\"\n Exit 1\n }\n\n $processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/quiet /norestart\"\n PassThru = $true\n Wait = $true\n NoNewWindow = $true\n }\n\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n Write-Host \"Install exit code: $exitCode\"\n\n # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated\n if ($exitCode -eq 3010 -or $exitCode -eq 1641) {\n Exit 0\n }\n\n Exit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n",
"661ee74e": "# Uninstalls the Microsoft .NET Runtime WiX \"burn\" bundle.\n#\n# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry\n# (keyed by the bundle ProductCode) alongside several MSI component entries that\n# share the same DisplayName. Only the bundle entry removes the whole runtime, and\n# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via\n# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).\n# We target the bundle by its ProductCode (injected by the ingester) and fall back\n# to the cached bootstrapper in the Package Cache.\n\n$productCode = '{866BECDA-F284-473A-9E84-0CCE816BF06F}'\n\nfunction Invoke-Uninstaller {\n param([string]$exe, [string]$exeArgs)\n if ($exeArgs -notmatch '/uninstall') { $exeArgs = \"/uninstall $exeArgs\" }\n if ($exeArgs -notmatch '/quiet') { $exeArgs = \"$exeArgs /quiet\" }\n if ($exeArgs -notmatch '/norestart') { $exeArgs = \"$exeArgs /norestart\" }\n $exeArgs = $exeArgs.Trim()\n Write-Host \"Uninstall command: $exe\"\n Write-Host \"Uninstall args: $exeArgs\"\n $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait\n return $process.ExitCode\n}\n\n$exitCode = $null\n\n# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its\n# UninstallString/QuietUninstallString points to the cached bootstrapper .exe.\n$keys = @(\n \"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$productCode\",\n \"HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$productCode\"\n)\n\nforeach ($key in $keys) {\n if (-not (Test-Path $key)) { continue }\n $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue\n if (-not $entry) { continue }\n\n $raw = $entry.QuietUninstallString\n if (-not $raw) { $raw = $entry.UninstallString }\n if (-not $raw) { continue }\n\n # Parse into executable + args, handling quoted/unquoted/bare shapes.\n if ($raw -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $exe = $matches[1]; $exeArgs = $matches[2].Trim()\n } elseif ($raw -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $exe = $matches[1]; $exeArgs = $matches[2].Trim()\n } else {\n $exe = $raw; $exeArgs = \"\"\n }\n\n $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs\n break\n}\n\n# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which\n# burn names after the bundle ProductCode.\nif ($null -eq $exitCode) {\n $cached = Get-ChildItem -Path \"C:\\ProgramData\\Package Cache\\$productCode\" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1\n if ($cached) {\n $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs \"\"\n }\n}\n\nif ($null -eq $exitCode) {\n Write-Host \"Uninstall entry not found for product code: $productCode\"\n Exit 0\n}\n\nWrite-Host \"Uninstall exit code: $exitCode\"\n# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated\nif ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }\nExit $exitCode\n"
}
}
@@ -1792,6 +1792,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = {
"menubar stats": MenubarStats,
menubarx: Menubarx,
"merlin project": MerlinProject,
"microsoft .net desktop runtime": MicrosoftDotnetRuntime,
"microsoft .net runtime": MicrosoftDotnetRuntime,
"microsoft 365 copilot": Microsoft365Copilot,
"microsoft auto update": MicrosoftAutoUpdate,
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB