Files
fleet/articles/detect-and-remove-peripheral-software.md
T
|@rm!n3 a980d4e2f0 Revise article on detecting and removing unwanted software (#49755)
Updated the article to improve clarity and fix minor grammatical issues
throughout the text, including prerequisites, policy creation, and
script instructions.

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [ ] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

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

For unreleased bug fixes in a release candidate, one of:

- [ ] Confirmed that the fix is not expected to adversely impact load
test results
- [ ] Alerted the release DRI if additional load testing is needed

## Database migrations

- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [ ] Verified that the setting is exported via `fleetctl
generate-gitops`
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [ ] Verified that any relevant UI is disabled when GitOps mode is
enabled

## fleetd/orbit/Fleet Desktop

- [ ] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [ ] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [ ] Verified that fleetd runs on macOS, Linux and Windows
- [ ] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))
2026-07-22 11:03:50 -05:00

21 KiB

Detect and remove unwanted software installed by peripherals in Fleet

When you plug in a monitor, docking station, or printer, Windows can quietly install companion software, from monitor utilities that push trialware ads to docking station managers nobody asked for. The recent LG Monitor App story (July 2026) made headlines, but the broader problem is older: peripherals bundling junkware that lands on corporate workstations.

This guide walks through detecting and automatically removing unwanted software using Fleet policies and scripts. It covers traditional Windows installers (MSI/EXE) and Store-installed MSIX apps like the LG Monitor App.

Prerequisites

Check these before you start:

  • Fleet Premium for policy automation scripts. Script automations (triggering remediation when a policy fails) require Fleet Premium. Fleet Free users can create detection policies and run scripts manually from the Fleet UI.
  • Windows hosts enrolled in Fleet. Detection uses the programs table for both MSI/EXE software and Store (MSIX) apps.
  • Fleet's agent updated for full Store app visibility. Store (MSIX) apps appear in the programs table once the agent is recent enough. Support for MSIX packages arrived in osquery 5.17.0, and osquery 5.22.1 fixed a gap where provisioned or never-launched Store apps were invisible. Check the version on your hosts with a live query: SELECT version FROM osquery_info;. Update Fleet's agent (fleetd) if a host reports a version below 5.22.1.
  • Scripts enabled. If you use Fleet's MDM features, scripts are enabled by default. If you deploy fleetd without MDM, pass the --enable-scripts flag during installation.

Create a policy to detect unwanted software (MSI/EXE)

In Fleet, a policy passes when its query returns at least one row, and fails when it returns zero rows. To detect unwanted software, invert the logic: return a row when the software is NOT present.

  1. Navigate to Policies and click Add policy.
  2. In the Name field, enter "McAfee trial software detected."
  3. In the Query field, paste the following SQL:
SELECT 1 WHERE NOT EXISTS (
  SELECT 1 FROM programs WHERE name LIKE '%McAfee%'
);
  1. In the Resolution field, add instructions for your help desk: "McAfee trial software was detected and has been automatically removed. Contact IT if you were expecting to use McAfee products on this machine."

This query returns a row (pass) when no McAfee software exists. When McAfee is found, the subquery returns results, NOT EXISTS evaluates to false, and the outer query returns zero rows, so the policy fails and triggers any attached automation.

Note: The programs table reads the Windows Uninstall registry keys (both MSI and EXE installers that register in Add/Remove Programs). Recent versions of Fleet's agent also include Store (MSIX) apps in this table. See the Store apps section below for how to target those precisely.

Create a script to remove unwanted software

  1. Navigate to Controls > Scripts and click Add script.
  2. Name the script "Remove McAfee trial software" and set Platform to Windows.
  3. In the Script field, paste the following PowerShell:
# Find McAfee entries in the Windows Uninstall registry and remove them
$uninstallPaths = @(
    "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
    "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
)

$found = $false
$failed = $false

# Exit codes that indicate a successful uninstall:
# 0 = success, 3010 = success but reboot required (common with /norestart),
# 1641 = success, reboot initiated by the installer.
$successCodes = @(0, 3010, 1641)

# Parse an uninstall string into executable + arguments and run it.
# Handles quoted paths with spaces (e.g. "C:\Program Files\...\uninstall.exe" /S)
# without laundering the command through cmd.exe.
function Invoke-Uninstaller {
    param(
        [string]$CommandLine,
        [string]$ExtraArgs = ""
    )

    if ($CommandLine -match '^"([^"]+)"\s*(.*)$') {
        $exe = $matches[1]
        $argString = $matches[2]
    } else {
        $parts = $CommandLine -split '\s+', 2
        $exe = $parts[0]
        $argString = if ($parts.Count -gt 1) { $parts[1] } else { "" }
    }

    if ($ExtraArgs) {
        $argString = ("$argString $ExtraArgs").Trim()
    }

    if ([string]::IsNullOrWhiteSpace($argString)) {
        $proc = Start-Process -FilePath $exe -NoNewWindow -Wait -PassThru
    } else {
        $proc = Start-Process -FilePath $exe -ArgumentList $argString -NoNewWindow -Wait -PassThru
    }
    return $proc.ExitCode
}

foreach ($basePath in $uninstallPaths) {
    if (-not (Test-Path $basePath)) { continue }

    Get-ChildItem $basePath | ForEach-Object {
        $entry = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
        if (-not $entry) { return }
        if ($entry.DisplayName -notlike '*McAfee*') { return }

        $found = $true
        $name = $entry.DisplayName
        Write-Output "Found: $name"

        # MSI uninstall: rebuild the command from the product GUID.
        # Matches both /I{GUID} (modify) and /X{GUID} (uninstall) registrations,
        # including quoted msiexec paths like "C:\Windows\system32\msiexec.exe" /I{GUID}.
        if ($entry.UninstallString -match 'MsiExec\.exe"?\s*/(I|X)\s*\{(.+?)\}') {
            $guid = $matches[2]
            Write-Output "Uninstalling MSI: $name"
            $proc = Start-Process "MsiExec.exe" -ArgumentList "/X{$guid} /qn /norestart" -NoNewWindow -Wait -PassThru
            if ($proc.ExitCode -notin $successCodes) {
                Write-Output "Failed: exit code $($proc.ExitCode)"
                $failed = $true
            } elseif ($proc.ExitCode -eq 3010) {
                Write-Output "Uninstalled: $name (reboot required)"
            }
            return
        }

        # Quiet uninstall string (preferred, already includes silent flags)
        if ($entry.QuietUninstallString) {
            Write-Output "Uninstalling $name (quiet)"
            $code = Invoke-Uninstaller -CommandLine $entry.QuietUninstallString
            if ($code -notin $successCodes) {
                Write-Output "Failed: exit code $code"
                $failed = $true
            }
            return
        }

        # Standard uninstall string. Caveat: may not support silent mode
        if ($entry.UninstallString) {
            Write-Output "Uninstalling $name (standard, may not be silent)"
            $code = Invoke-Uninstaller -CommandLine $entry.UninstallString -ExtraArgs "/quiet /norestart"
            if ($code -notin $successCodes) {
                Write-Output "Failed: exit code $code"
                $failed = $true
            }
            return
        }
    }
}

if (-not $found) {
    Write-Output "No McAfee software found to remove."
    exit 0
}

if ($failed) {
    Write-Output "One or more uninstallations failed. Check output above."
    exit 1
}

Write-Output "McAfee removal complete."
exit 0

This script reads both the 64-bit and 32-bit Uninstall registry hives, rebuilds MSI uninstall commands from the product GUID (handling both /I and /X registrations, quoted or unquoted msiexec paths), and parses EXE uninstall strings into executable-plus-arguments so paths with spaces, like anything under Program Files, launch correctly. It prefers QuietUninstallString when available and falls back to the standard UninstallString with appended quiet flags. Exit codes 3010 and 1641 (success, reboot required) are treated as success so a completed uninstall doesn't trigger a spurious retry. The script returns non-zero only on real failures, so Fleet's 3-retry mechanism triggers correctly.

  1. Click Save to create the script.

Warning: Test this on a single host first. Run the script manually against one machine and check the script output in that host's activity feed to confirm it uninstalls the right software before automating fleet-wide.

Note: If your organization legitimately uses McAfee/Trellix Endpoint Security on some machines, narrow the scope. Replace *McAfee* with *McAfee Trial* or *McAfee Safe Search* to avoid removing production security software.

Note: Appending /quiet /norestart to arbitrary EXE uninstallers doesn't always work. NSIS installers want /S, Inno Setup wants /VERYSILENT. If an uninstaller lacks quiet support, it will prompt for UI and hang in Fleet's non-interactive SYSTEM context until the script timeout. For stubborn software, use vendor-specific removal tools (for example, McAfee's MCPR tool) deployed as a Fleet software package.

Connect the policy and script with automation

  1. Navigate to Policies and click Manage automations.
  2. Find your "McAfee trial software detected" policy and select it.
  3. In the automation panel, choose Run script and select "Remove McAfee trial software."
  4. Click Save.

When any Windows host fails the McAfee policy, Fleet runs the uninstall script. The script runs up to 3 times total, retriggering each time it exits with a non-zero code. After removal, the next policy evaluation passes.

Note: Policy automations attach to policies scoped to a specific fleet (team), not global policies. If you organize hosts by fleet, create the policy at that level and attach the script there.

Detect Store apps (MSIX) like the LG Monitor App

Companion apps like the LG Monitor App and Alienware Command Center install as MSIX packages from the Microsoft Store, with no user action required. Once Fleet's agent is recent enough, MSIX packages appear in the programs table with a populated package_family_name column. Support arrived in osquery 5.17.0, and osquery 5.22.1 closed the remaining gap where apps that no user had launched were missing from inventory. The same policy pattern works here: match on the package family name rather than the display name, since it's the stable identifier.

Policy

  1. Navigate to Policies and click Add policy.
  2. In the Name field, enter "LG Monitor App (Store) detected."
  3. In the Query field, paste:
SELECT 1 WHERE NOT EXISTS (
  SELECT 1 FROM programs
  WHERE package_family_name LIKE 'LGElectronics.LGMonitorApp%'
);

This returns zero rows (fail) when the LG Monitor App package is present on the host.

Note: To find the package family name for any Store app, run Get-AppxPackage -AllUsers | Select Name, PackageFamilyName on an affected host, or query SELECT name, package_family_name FROM programs WHERE package_family_name != '' via Fleet live query. Vendors sometimes ship a separate installer stub package alongside the app itself. Check for related packages (for example, names containing "Installer") and widen the LIKE pattern if you find one.

Note: Querying MSIX data in programs involves enumerating installed packages through the Windows Appx APIs, which is slower than the registry reads used for MSI/EXE entries. Policy evaluations run on a schedule (default hourly), so this doesn't affect end users, but live queries can take longer on hosts with many Store apps.

Removal script

  1. Navigate to Controls > Scripts and click Add script.
  2. Name the script "Remove LG Monitor App and prevent reinstall" and set Platform to Windows.
  3. In the Script field, paste:
$failed = $false

# Remove the LG Monitor App (Store/MSIX package) for all users.
# Wildcard also catches related packages (e.g. installer stubs).
$packages = Get-AppxPackage -Name "LGElectronics.LGMonitorApp*" -AllUsers -ErrorAction SilentlyContinue

if ($packages) {
    foreach ($package in $packages) {
        Write-Output "Removing $($package.Name) version $($package.Version)"
        try {
            $package | Remove-AppxPackage -AllUsers -ErrorAction Stop
            Write-Output "App removed."
        } catch {
            Write-Output "App removal failed: $($_.Exception.Message)"
            $failed = $true
        }
    }
} else {
    Write-Output "LG Monitor App not found (may already be removed)."
}

# Remove the provisioned package so it isn't installed for new users
try {
    $provisioned = Get-AppxProvisionedPackage -Online -ErrorAction Stop |
        Where-Object { $_.DisplayName -like "*LGMonitorApp*" }
} catch {
    $provisioned = $null
}

if ($provisioned) {
    try {
        $provisioned | Remove-AppxProvisionedPackage -Online -ErrorAction Stop | Out-Null
        Write-Output "Provisioned package removed."
    } catch {
        Write-Output "Provisioned package removal failed: $($_.Exception.Message)"
        $failed = $true
    }
}

# Remove LG's driver-store delivery packages. LG ships SoftwareComponent
# driver packages matched to monitor hardware IDs whose job is to re-trigger
# the Store install. They survive app removal and re-arm the install cycle,
# so removing the app alone is not enough. Removing them does not affect
# basic monitor functionality.
try {
    $lgDrivers = Get-WindowsDriver -Online -ErrorAction Stop |
        Where-Object {
            $_.ProviderName -like "LG Electronics*" -and
            $_.ClassName -in @("SoftwareComponent", "Extension")
        }
} catch {
    $lgDrivers = @()
    Write-Output "Could not enumerate the driver store: $($_.Exception.Message)"
}

foreach ($drv in $lgDrivers) {
    Write-Output "Removing driver package $($drv.Driver) ($($drv.OriginalFileName))"
    $null = pnputil /delete-driver $drv.Driver /uninstall /force
    if ($LASTEXITCODE -notin @(0, 3010)) {
        Write-Output "Failed to remove $($drv.Driver): pnputil exit code $LASTEXITCODE"
        $failed = $true
    }
}

# Belt and suspenders: block device metadata retrieval, which is one of the
# channels Windows uses to deliver companion apps for connected hardware.
# Note: this does NOT block installs triggered by driver-store packages.
# That's what the pnputil cleanup above is for.
$policyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata"
if (-not (Test-Path $policyPath)) {
    New-Item -Path $policyPath -Force | Out-Null
}
$existing = (Get-ItemProperty -Path $policyPath -Name "PreventDeviceMetadataFromNetwork" -ErrorAction SilentlyContinue).PreventDeviceMetadataFromNetwork
if ($existing -eq 1) {
    Write-Output "Device metadata retrieval already disabled."
} else {
    Set-ItemProperty -Path $policyPath -Name "PreventDeviceMetadataFromNetwork" -Value 1 -Type DWord
    Write-Output "Device metadata retrieval disabled."
}

if ($failed) { exit 1 }
exit 0
  1. Save the script and attach it to the Store app policy via Manage automations.

Warning: Before deploying, run pnputil /enum-drivers on an affected host and confirm the LG delivery packages' provider name and class. Adjust the ProviderName filter if your hosts report a different string. Only SoftwareComponent and Extension class packages are targeted, so display drivers are untouched.

Warning: Disabling device metadata retrieval blocks companion app installation via device metadata for ALL hardware, including legitimate ones your users may want. Scope this to specific fleets rather than applying it globally.

Note: PreventDeviceMetadataFromNetwork is also settable through Windows MDM as an ADMX-backed policy (./Device/Vendor/MSFT/Policy/Config/DeviceInstallation/PreventDeviceMetadataFromNetwork). If you manage Windows hosts with Fleet MDM, a custom configuration profile is the more durable option: profiles are re-enforced, while a script sets the value once. The script approach above works on hosts without MDM enrollment.

Get notified when unwanted software is detected

Fleet sends webhook notifications when a host transitions to a failing policy state. Webhooks fire once per day by default, not immediately. To send Slack notifications, you need a transform layer because Fleet's JSON payload doesn't match Slack's expected {"text": "..."} format:

  1. Set up an incoming webhook in Tines, Zapier, or a Lambda function that transforms Fleet's payload into Slack format.
  2. Navigate to Policies > Manage automations, enable the webhook workflow, select your policy, and enter your transform layer's URL.

Note: Webhook notifications are available on Fleet Free. Script automations require Fleet Premium.

Adapt this for other peripheral-installed software

The same pattern works for any unwanted software:

  • Docking station utilities. DisplayLink Manager, Plugable utilities, and other dock software show up in programs. Use the policy template and scope by publisher plus a specific product name.
  • Printer bundles. Canon, Epson, and Brother utilities follow the same approach. Scope by publisher to avoid hitting unrelated software.
  • Monitor companion apps (Store). Alienware Command Center auto-installs via the same mechanisms. Use the same package_family_name LIKE '...' pattern. Run Get-AppxPackage -AllUsers | Select PackageFamilyName on an affected host to get the exact prefix.

Note: Avoid broad substring matches like %HP% in the name field, since they hit unrelated programs. Scope on publisher or use specific product names.

Verify the cleanup worked

  1. Navigate to Software and search for "McAfee" in the software inventory.
  2. Confirm the number of affected hosts drops to zero as policies evaluate.

You can also run a live query from Queries:

SELECT name, version, publisher, install_date FROM programs WHERE name LIKE '%McAfee%';

If no results return, the software is fully removed from your fleet.

Troubleshoot

Policy automation didn't trigger for hosts that were already failing.

Automations fire on transition (newly failing: no-response-to-fail or pass-to-fail). Hosts that were already failing won't trigger. To force a recheck: deselect the policy in Manage automations, click Save, then reselect it. This resets the host counts and re-triggers the automation immediately.

Store app doesn't appear in the software inventory.

Check the version reported on the host (SELECT version FROM osquery_info;). MSIX support in the programs table requires osquery 5.17.0, and apps that no user has launched, the normal state for auto-installed companion apps, require 5.22.1. On older versions, update Fleet's agent (fleetd) to bring Store apps into inventory.

Script hangs or times out on some hosts.

If an uninstaller lacks quiet/silent flags, it may prompt for UI input, which fails in Fleet's non-interactive SYSTEM context and hangs until the script timeout. The timeout is an agent option (script_execution_timeout under agent_options, default 300 seconds, maximum 18000), settable through the Fleet UI or GitOps. For stubborn software, use vendor-specific removal tools deployed as Fleet software packages.

Store app keeps reinstalling after removal.

Windows has two delivery channels that can re-trigger the install when the user reconnects the peripheral. The first is device metadata: Windows matches the hardware to a companion app listing and installs it. The PreventDeviceMetadataFromNetwork policy blocks this channel. The second is driver-store delivery: the vendor ships a SoftwareComponent driver package (via Windows Update, matched to hardware IDs) whose only job is to install the Store app. The metadata policy does NOT block this channel. The driver package must be removed from the driver store with pnputil, which the removal script above does. If the app still returns, check pnputil /enum-drivers output for vendor packages the script's filter missed, and check whether Windows Update re-delivered the driver package (block it with a driver group policy or WSUS/WUfB deferral if so).

Automation retry limit reached.

Script automations attempt up to 3 times, retriggering on non-zero exit codes. If all 3 fail, Fleet stops retrying. Check the script output in the host's activity feed to see why it failed. In Fleet Premium, set continuous_automations_enabled: true on the policy to trigger on every evaluation, including fail-to-fail transitions.

Further reading