Detect Missing Required Apps with PowerShell · CloudEngineerHub
Detection & Remediation8 min read4 views
Detecting Missing Required Applications Across Your Fleet with PowerShell
A detection-only script that reads machine and per-user uninstall registry keys, matches your required-app patterns, and exits non-zero when apps are missing.
C
CloudEngineerHub Team
Published: September 12, 2026· Last Updated: September 28, 2026·8 min read
Every organization has a short list of software that must be present: the security agent, the VPN client, the PDF reader, the in-house tool that payroll runs on. Intune tells you what it deployed; it is much worse at telling you what actually survived. Users uninstall things, installers fail halfway, gold images drift from their reference builds, and months later a compliance audit asks why three hundred devices are missing the encoder everyone assumed was universal.
Visual Walkthrough
The engine behind every “Detect with PowerShell” runbook on this site.
Exit 1 flips the device into the remediation queue for this package.
remediate
3
Remediation executes
Fix script runs silently as SYSTEM with a 5-minute budget.
verify
4
Re-run detection
The same detection script must now exit 0 — proof, not hope.
telemetry
5
Results report
Per-device outcome lands in Reports → Remediations for trend analysis.
6
Repeat on schedule
The loop continues — drift gets caught the day it appears.
Continuous cycle
Proactive Remediations — The Detect → Fix → Verify Loop. 1. Detection script runs: Scheduled (daily/hourly): exit 0 healthy, exit 1 unhealthy — nothing more. — 2. Verdict: unhealthy: Exit 1 flips the device into the remediation queue for this package. — 3. Remediation executes: Fix script runs silently as SYSTEM with a 5-minute budget. — 4. Re-run detection: The same detection script must now exit 0 — proof, not hope. — 5. Results report: Per-device outcome lands in Reports → Remediations for trend analysis. — 6. Repeat on schedule: The loop continues — drift gets caught the day it appears.
C
CloudEngineerHub Team
Author
The CloudEngineerHub editorial team — Microsoft endpoint management and cloud engineers writing hands-on guides on Intune, Azure, Entra ID, Autopilot, Defender and PowerShell. Every article is reviewed against current Microsoft documentation.
This post ships a detection-only script that answers that question per device, on a schedule, using the same registry data that "Programs and Features" itself reads. You define the required apps as a list of display-name patterns in the script's parameter block, the script collects every installed product name it can find - machine-wide and per-user - and exits non-zero when a pattern matches nothing. Intune then surfaces those devices in the package's device status view, which is your missing-software worklist.
One honest limitation to set expectations early: this is a display-name matcher against the uninstall registry, not a full software inventory. Applications that write no uninstall entry, or hide it with SystemComponent, are invisible to any tool that reads these keys, this one included. For the apps that matter, that is rarely an issue - properly packaged enterprise software writes these entries.
Symptoms
Missing required software produces downstream symptoms long before anyone checks inventory:
Users open tickets for workflows that "suddenly stopped working" on specific devices.
Security or compliance scans report endpoint gaps on a shifting subset of machines.
A vendor's support tool reports devices that stopped phoning home weeks ago.
After an OS refresh wave, some devices simply lack applications that were on the old image.
Manual spot checks with appwiz.cpl confirm the app is gone, and nobody knows when it left.
If you recognize two or more of these, a scheduled detection package will convert the vagueness into a device list.
Possible Causes
Users uninstalling what they should not
Standard users can remove many per-user installs without any elevation. Chat tools, readers, and agents installed in user context vanish the day the user decides they dislike them.
Failed or partial deployments
An install that failed after the machine-wide registration was written - or before it - leaves a device in an inconsistent state that inventory reports as missing.
Image drift
Golden images that were not re-baselined after application updates produce new devices that carry old versions with different display names, which pattern matching then reports as missing.
Installer changes between versions
A vendor changing its product name between major versions ("Contoso Backup" becoming "Contoso Data Protection") silently breaks exact-name matching. Wildcard patterns absorb most of this.
32-bit vs 64-bit registry views
32-bit installers write their uninstall entries under the WOW6432Node. Any inventory routine that reads only the native view misses them - a classic reason homegrown scripts underreport.
Per-user installs invisible to machine scans
Many modern apps install per user by default. A script that only reads machine-wide keys misses them entirely, which is why this script reads loaded user hives too.
Detection Script
The script below is read-only and runs in seconds as SYSTEM in 64-bit Windows PowerShell. The required-apps list lives in the param block as RequiredApps - an array of display-name patterns with wildcard support - so editing your list means editing one line. It collects installed names from the three standard uninstall locations plus loaded per-user hives, then reports every pattern that matched nothing.
PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string[]] $RequiredApps = @('7-Zip*', 'Microsoft Edge')
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<#
.SYNOPSIS
Detection script (detection-only): compares installed software - read
from the Windows uninstall registry keys - against a required-apps
list of display-name patterns, and flags anything missing.
.NOTES
Intended for Intune Scripts and Remediations, 64-bit SYSTEM context.
This script NEVER changes the system and is detection-only.
Exit 0 = every required pattern matched an installed product,
1 = at least one required app is missing,
2 = misconfiguration - the required list is empty,
3 = unexpected error.
Context note: under SYSTEM, HKCU points at the SYSTEM account's own
hive, so per-user installs are read from the loaded profiles under
HKEY_USERS instead. Hives that are not loaded (their user is not
logged on since boot) cannot be inspected - see the article text.
#>$logDir = 'C:\ProgramData\Company\Logs'$logFile = Join-Path $logDir'RequiredAppDetection.log'function Write-DetectionLog {
param([Parameter(Mandatory)][string] $Level, [Parameter(Mandatory)][string] $Message)
if (-not (Test-Path $logDir)) { New-Item -Path$logDir -ItemType Directory -Force | Out-Null }
"$([DateTime]::UtcNow.ToString('o')) [$Level] $Message" | Out-File -FilePath$logFile -Append -Encoding utf8
}
function Get-InstalledDisplayNames {
# Collects DisplayName values from the standard uninstall keys: # machine-wide (64-bit and 32-bit views) and every loaded per-user hive. # Returns a deduplicated, case-insensitive list of names.param()
$displayNames = @{}
$roots = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
# Loaded per-user hives under HKEY_USERS. Running as SYSTEM, HKCU is the # SYSTEM account's own hive - the interactive users' products live here.$userRoots = @(Get-ChildItem -Path'Registry::HKEY_USERS' -ErrorAction SilentlyContinue |
Where-Object { $_.PSChildName -like'S-1-5-21-*' -and$_.PSChildName -notlike'*_Classes' } |
ForEach-Object { 'Registry::HKEY_USERS\' + $_.PSChildName + '\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' })
$roots = @($roots + $userRoots)
foreach ($rootin$roots) {
if (-not (Test-Path $root)) { continue }
$entryKeys = @(Get-ChildItem -Path$root -ErrorAction SilentlyContinue)
foreach ($entryKeyin$entryKeys) {
$props = Get-ItemProperty -Path$entryKey.PSPath -ErrorAction SilentlyContinue
if ($null -eq$props) { continue }
$displayNameProp = $props.PSObject.Properties['DisplayName']
if ($null -eq$displayNameProp) { continue }
$displayName = [string]$displayNameProp.Value
if ($displayName.Trim().Length -eq0) { continue }
# SystemComponent=1 marks hidden framework/update entries that # are noise for application inventory, so they are skipped.$systemComponentProp = $props.PSObject.Properties['SystemComponent']
if ($null -ne$systemComponentProp) {
$componentValue = 0if ([int]::TryParse([string]$systemComponentProp.Value, [ref]$componentValue) -and$componentValue -eq1) { continue }
}
$displayNames[$displayName] = $true
}
}
return @($displayNames.Keys)
}
try {
Write-DetectionLog 'INFO''Required app detection run started.'if ($RequiredApps.Count -eq0) {
Write-DetectionLog 'ERROR''RequiredApps is empty - configure the required display-name patterns before assigning this package.'
exit 2
}
$installedNames = Get-InstalledDisplayNames
Write-DetectionLog 'INFO'"Found $($installedNames.Count) distinct installed display name(s) across machine-wide and loaded user hives."$missing = @()
foreach ($patternin$RequiredApps) {
$matched = @($installedNames | Where-Object { $_ -like$pattern })
if ($matched.Count -gt0) {
Write-DetectionLog 'INFO'"Required pattern '$pattern' matched: $($matched -join ', ')"
}
else {
Write-DetectionLog 'ERROR'"Required pattern '$pattern' matched no installed product."$missing += $pattern
}
}
if ($missing.Count -gt0) {
Write-DetectionLog 'ERROR'"Missing required apps: $($missing -join ', ')"
exit 1
}
Write-DetectionLog 'INFO''All required apps are present.'
exit 0
}
catch {
Write-DetectionLog 'ERROR'"Unexpected failure: $($_.Exception.Message)"
exit 3
}
finally {
Write-DetectionLog 'INFO''Required app detection run ended.'
}
Detection Logic
Machine-wide names. The script enumerates subkeys of HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall and HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall. Reading both views is what makes 32-bit applications visible on 64-bit Windows. Each entry's DisplayName value is collected; entries without a display name are skipped because they cannot match anything meaningful.
Per-user names. Under SYSTEM, HKCU is the SYSTEM account's own hive - a detail that quietly breaks many inventory scripts. The interactive users' registered software lives under their profile hives in HKEY_USERS, so the script enumerates loaded user profiles (SIDs of the S-1-5-21-* form, excluding _Classes) and reads each one's uninstall key. Only loaded hives are visible; a user who has not logged on since boot is not inspected, which the article's production notes treat as a reporting caveat rather than hiding it.
Noise filtering. Entries marked SystemComponent = 1 are hidden framework, update, and runtime entries that flood inventory lists with items administrators cannot act on. The script skips them; if your required app hides itself this way, the caveat in the production notes applies.
Pattern matching. Each RequiredApps pattern is matched case-insensitively against the deduplicated name list using -Like, so '7-Zip*' matches any version and display-name suffixes stop mattering. Every match is logged with the actual names that satisfied it, which is how you catch vendor renames early.
Verdict. Any pattern with zero matches adds the device to the missing list and forces exit 1. All patterns matched - exit 0. An empty required list is a configuration defect and exits 2 rather than pretending to check nothing.
The log line per run is deliberately readable: patterns, matches, and a final missing list, so the package's device status view pairs cleanly with the log when you triage.
Remediation Approach
This post is detection-only, and the separation is intentional: detecting that an app is missing is a local, factual check, but installing the app correctly is a deployment problem with an Intune-native answer. If the app is missing because it was never assigned, the remediation is the assignment itself. If it is missing because users removed a per-user install, the durable fix is a required Win32 app deployment with the right install context, not a script that reinstalls on a loop. If the app was uninstalled by another tool, you have a policy conflict to resolve first - a reinstallation script would just fight it.
Use this package as the reporting layer over whatever deployment mechanism your organization already runs: assign the detection daily, feed exit-1 devices into your existing app deployment groups, and let the platform's own application pipeline do the installs it was designed for.
Deploying Through Intune
Deploy the detection script alone:
Open the Microsoft Intune admin center and go to Devices → Scripts and remediations.
Select + Add on the Windows 10/11 platform tab.
On Basics, name it Detect - Missing required apps and note in the description which app list it enforces visibility for, so future admins know to update the script when the list changes.
On the Detection script tab, upload detect.ps1. Set Run script in 64-bit PowerShell: Yes and Run this script using signed-on credentials: No - the HKEY_USERS enumeration requires the SYSTEM context to see all loaded profiles.
On the Remediation script tab, select No remediation script; installation belongs to your app deployment pipeline.
On Schedule, choose Daily. Missing software is not an emergency, but daily detection keeps the device list fresh enough to act on weekly review cycles.
On Assignments, target the device groups whose application baseline the list represents. If different device types need different app lists, create one package per baseline rather than one union list. Leave Enforce script signature check: No unless you sign and distribute certificates.
Review and create.
Post-specific notes:
Edit RequiredApps in the script before assigning. Patterns are -Like wildcards against display names; verify each pattern against one known-good device's installed list first.
Devices shared by multiple users are evaluated against machine-wide names plus whatever profiles are loaded at run time - acceptable for most baselines, worth knowing for strict ones.
Exit 2 means the list is empty: that is an assignment defect to fix in the script copy you uploaded, not a device condition.
Visual Walkthrough
From source files to a detected, reported installation via the IME.
Interactive FlowWin32 App Pipeline — Content Prep to Client Install6/6
1
Stage source files
MSI/EXE/PS1 plus install/uninstall commands in a clean folder.
package
2
Content Prep Tool
Wraps the folder into .intunewin with metadata envelope.
MSI code, file/registry, or script exit 0 = installed.
5
Assign required
Required install to device group; IME wakes and downloads.
6
Install → detect → report
IME executes, detection confirms, state flows to reports.
Win32 App Pipeline — Content Prep to Client Install. 1. Stage source files: MSI/EXE/PS1 plus install/uninstall commands in a clean folder. — 2. Content Prep Tool: Wraps the folder into .intunewin with metadata envelope. — 3. Upload app + program: Install behavior, return codes (3010 handled), requirements rules. — 4. Detection rules: MSI code, file/registry, or script exit 0 = installed. — 5. Assign required: Required install to device group; IME wakes and downloads. — 6. Install → detect → report: IME executes, detection confirms, state flows to reports.
Exit Codes
Intune treats any non-zero detection exit as "remediation needed"; with no remediation script attached, the code drives reporting only:
Code
Meaning
0
Every required pattern matched at least one installed product
1
At least one required pattern matched nothing - the missing list is in the log
2
Misconfiguration - the required list is empty
3
Unexpected script error - inspect the log
A device can be missing several apps at once; the log line names all missing patterns in one entry, so a single run gives you the complete gap for that device.
Logging
The script appends to C:\ProgramData\Company\Logs\RequiredAppDetection.log, creating the directory on first run. Each line carries a UTC timestamp, a level, and a message. Per run you get: the count of distinct installed names found, one line per required pattern with its matches, an ERROR line per missing pattern, and a summary missing list. Because display names drift with vendor updates, the matched-name lines double as a change history for your application naming. Growth is a few lines per run - rotate monthly if you keep ProgramData logs bounded. For a full installer-level view of a single device, the registry paths themselves (Programs and Features data) can be browsed directly during a hands-on session.
Troubleshooting the Script
A known-installed app is reported missing. Check the exact display name on a reference device and widen the pattern. Vendor renames are the most common cause; the second most common is matching against the executable name rather than the display name.
Per-user apps seem invisible on some devices. Their user profiles were not loaded when the script ran - typically devices where nobody has logged in since boot. The script cannot see unloaded hives; consider a machine-wide deployment for strictly enforced apps.
The report is flooded with framework entries. That should not happen - SystemComponent=1 entries are skipped. If you still see runtime noise, tighten your patterns rather than editing the filter.
Exit 3 with a registry error. A third-party registry filter or a corrupted uninstall key can abort enumeration. The log's error message identifies the failing path; exclude that key from investigation or repair it locally.
No log file appears. The script never ran: check assignment, check-in, and 64-bit context, as with any remediation package.
How to Validate the Fix
On a lab device with all baseline apps installed, run the script in an elevated 64-bit console. Expect exit 0 and one matched line per pattern.
Add a deliberately bogus pattern such as 'Definitely Not Installed*' to RequiredApps and re-run. Expect exit 1 with that pattern in the missing list.
Uninstall a real (harmless) app from the baseline on the lab device, re-run, and confirm it is the one reported missing - this proves your patterns track reality.
Reinstall the app (or deploy it through your normal pipeline), re-run detection, and confirm exit 0.
In the admin center, confirm the Device status view now shows the lab device as compliant after sync, and that previously flagged devices line up with your manual spot checks.
Production Considerations
Execution context. SYSTEM in 64-bit Windows PowerShell. The script reads registry data only; it never writes, never installs, and never removes anything.
Idempotency. Detection-only by design - repeated runs produce identical results on an unchanged device, and frequency carries no cost beyond a short registry read.
Side effects. None on the device. The main operational risk is a false "missing" from a too-narrow pattern, which the matched-name logging is there to catch during your pilot week.
Security. No credentials, no secrets, no network access; the script reads only software inventory data that any local administrator could read. Have your PowerShell automation SME and security team review the script and validate the pattern list in a test ring before you rely on its output for compliance reporting.
Scope control. Keep one package per app baseline and assign it to the matching device groups; a single union list across unlike devices produces noise you will learn to ignore, which defeats the purpose.
Why read the registry instead of using a real inventory tool?
Because the registry read is instant, dependency-free, and runs anywhere Windows PowerShell does - which makes it ideal as a scheduled, per-device compliance check feeding Intune reporting. Dedicated inventory tools remain the right answer for license reconciliation and version-level detail; this script answers the narrower question "is the required app present at all?"
Why does the script read HKEY_USERS instead of HKCU?
The script runs as SYSTEM, and in that context HKCU is the SYSTEM account's own registry hive - not the logged-on user's. Per-user software registrations live under each user's SID in HKEY_USERS, which is where the script looks. This is one of the most common mistakes in homegrown inventory scripts running from management agents.
What if an app hides itself with SystemComponent?
Then this matcher cannot see it, by design - those entries are precisely the noise the filter removes. For an app that important, switch to a Win32 app deployment with a proper detection rule, which verifies presence by file, registry, or script evidence rather than by display name.
Comments
0No comments yet — be the first to share your thoughts.