From 5fa8a0a9ea26bf74eeae9ce4ecbd0e57e3e35122 Mon Sep 17 00:00:00 2001 From: kitzy Date: Sun, 2 Aug 2026 23:10:40 -0400 Subject: [PATCH] Add Microsoft .NET Desktop Runtime 10 as a Windows FMA (#50361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 ## 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. --- .../microsoft-dotnet-desktop-runtime-10.json | 14 +++++++++++ ee/maintained-apps/outputs/apps.json | 7 ++++++ .../windows.json | 22 ++++++++++++++++++ .../SoftwarePage/components/icons/index.ts | 1 + ...oft-dotnet-desktop-runtime-10-60x60@2x.png | Bin 0 -> 1738 bytes 5 files changed, 44 insertions(+) create mode 100644 ee/maintained-apps/inputs/winget/microsoft-dotnet-desktop-runtime-10.json create mode 100644 ee/maintained-apps/outputs/microsoft-dotnet-desktop-runtime-10/windows.json create mode 100644 website/assets/images/app-icon-microsoft-dotnet-desktop-runtime-10-60x60@2x.png diff --git a/ee/maintained-apps/inputs/winget/microsoft-dotnet-desktop-runtime-10.json b/ee/maintained-apps/inputs/winget/microsoft-dotnet-desktop-runtime-10.json new file mode 100644 index 0000000000..e986046f41 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/microsoft-dotnet-desktop-runtime-10.json @@ -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"] +} diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 1a70f44b18..ba1396de85 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -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", diff --git a/ee/maintained-apps/outputs/microsoft-dotnet-desktop-runtime-10/windows.json b/ee/maintained-apps/outputs/microsoft-dotnet-desktop-runtime-10/windows.json new file mode 100644 index 0000000000..c1a829a8aa --- /dev/null +++ b/ee/maintained-apps/outputs/microsoft-dotnet-desktop-runtime-10/windows.json @@ -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" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index 83f58243c2..4aa9b0f9cc 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -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, diff --git a/website/assets/images/app-icon-microsoft-dotnet-desktop-runtime-10-60x60@2x.png b/website/assets/images/app-icon-microsoft-dotnet-desktop-runtime-10-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..55378b5ce47821065d6038deea8d9d7acbe382ce GIT binary patch literal 1738 zcmZ`&doAU$o~oUf5<0i5WM2m*itTW!A>02H5iHUM0K0KjS-06-f6z%sLW zAoMr@=thN{4)+apB!oB+LhK31Bcuy3N-~TR=RuD6nwaE6N%SIzITOR22=Sie7$^yG zh!A&z9BfZ$sbsHll-y+nZ$&XnkYtC=2HWAUM9wYF$+#;D9$P`06eoC*C*O*mG;`nn zSFps8T}9HL{K(zf+IoW%M7j_QV(2o7n!{2g`%q$!lNe;_K=*p2%MAYgrd+1Ek;s7C z;k%o--{&n##A^1UoKBJsbgh5Gm7=J`Y3NWXA&KsfeFSdUpc z10ifi={7+>oYW0?QLsgVA-*cafJW4w2K0_GaT%bz3n^w`W48fYR zwVE@86XZtCk*6f@282T|1Z8Opl1+8(wWSXuqN!2Q$Fa?pDt7pPh7 zS6xqFG!-vjhSQ!lbC(z7H@}-t@}|)6l8GTv19qvgXu0v`a@uLywRi?)T5>y!DV3=2 z7P2yeX`v2;ik!vtAllNr{Qc{V2X|H;*RQHo8ihimR%=vB4T~qaQqggY2nbRC*j`iZ@#!Q4`oRw1w#gf{g&`^+$k%Xnn}LC$ZIsz5 zLv70j%MIBlzu={%gDm6Vh2Nj>5`A^MZsOyoxhNxZdYF|b#FaUNkJfFH&zv6@UG9Tm{ zy#OJqA}a4KIDl?}cAYeL^q#vJ7wud3Wfs#rE2FdJkj`Nf*sNaGPE+JoUH?=a?yC|+ zf?9BW?_DW@*Ak)J!=4TiYGxx;RIJY&WWSck4W9W>^S9bkeXKRmOEHb6AD zHp)%`t@SsW}DM_oj3}&mF1uDu?Ml7bLoQoUbx8Jz`x0FyTM9WDxMhHr43m z?%JH%qh{N|<5!!I9s7*2Mt6oveUD#{GVnhGns7dcD!O51(gp0RuRuq{gem!XLv&*j z5%2aVy8q2F-CJ%KQ|PFr$MyQAzn{uuO>qyuvdt7(IJLS39vbAJitRCdG}PrM0SKKm z|L#{gmaYaNlP73ZE8yzrF0NjoU7Q{B<*PbngFX1twL!C#LfP2l;z9C$o%K}re~T+L z##_-q)`wV#{+|3!k-t}@EwW9%E8Yla^>Lf2ot-#j-aY0b)uO+`9^?Pcill41)1XK0 zMA9RLg@Knp|4k;7qU3>(n zp3h$Aa4!FuoAyiz?