ππͺπ»πCCScript - Windows - Detect RMM
What this script does πβ
This is a Custom Compliance Detection Script for Intune that checks if your RMM (Remote Monitoring & Management) agent is installed and actually running.
Because what's the point of remote management if the agent isn't... you know... managing anything?
The Challengeβ
Microsoft's built-in compliance checks are solid:
- BitLocker? β
- Secure Boot? β
- Firewall? β
But they don't check for your business-critical tools. Like RMM agents.
That's where Custom Compliance saves the day.
This script checks if your RMM tool is:
- Present β Installed on the device
- Active β Actually running (not just sitting there doing nothing)
If either check fails, the device is marked non-compliant in Intune.
How it works π οΈβ
Detection Strategyβ
The script uses multiple detection methods to find RMM agents:
- Process Detection β Is the RMM process running? (e.g.,
NinjaRMMAgent.exe) - Service Detection β Is the Windows service installed and started?
- File Path Detection β Do the installation files exist? (e.g.,
C:\Program Files\NinjaRMMAgent) - Registry Detection β Is it registered in Windows' Uninstall keys?
- Version Detection β What version is installed?
If any of these methods find the RMM, it's considered present.
If the service is running OR the process is running, it's considered active.
Supported RMM Toolsβ
The script auto-detects these RMM platforms:
- NinjaOne
- Datto RMM (formerly Autotask/CentraStage)
- TacticalRMM
- ConnectWise Automate (LabTech)
- N-able (SolarWinds RMM)
- Atera
- ManageEngine Endpoint Central (Desktop Central)
- Pulseway
- Splashtop
- TeamViewer
It checks them in priority order and reports the first one found.
Auto vs Manual Modeβ
Auto Mode (default):
# No configuration needed
# Script scans for all known RMM tools
# Reports the first one it finds
Manual Mode (optional):
# Set environment variable to force a specific RMM
$env:RMM_CHOICE = "NinjaOne"
This is useful if you have multiple RMM tools installed and want to check a specific one.
Output Format π€β
The script outputs a single-line JSON object to stdout:
{
"tool": "NinjaOne",
"present": true,
"active": true,
"serviceInstalled": true,
"serviceRunning": true,
"processRunning": true,
"pathExists": true,
"regInstalled": true,
"version": "5.8.1234",
"alsoDetected": [],
"timestampUtc": "2025-01-17T12:34:56.789Z",
"detailsCompressed": "H4sIAAAAAAAA..."
}
Key Fieldsβ
| Field | Type | Description |
|---|---|---|
tool | string | Which RMM was detected (or "Auto" if none found) |
present | boolean | Is the RMM installed? (Used by Intune compliance rule) |
active | boolean | Is the RMM running? (Used by Intune compliance rule) |
serviceInstalled | boolean | Is the Windows service registered? |
serviceRunning | boolean | Is the service currently running? |
processRunning | boolean | Is the process currently running? |
pathExists | boolean | Do installation files exist? |
regInstalled | boolean | Is it in the Uninstall registry? |
version | string | Version number (if detectable) |
alsoDetected | array | Other RMM tools found (if any) |
timestampUtc | string | When the check ran (ISO 8601 UTC) |
detailsCompressed | string | Full detection details (gzip + base64) |
The detailsCompressed field contains all the raw detection data (which processes, services, paths, registry keys were found) compressed for efficient storage.
Integration with Intune πβ
Upload the Scriptβ
In Intune:
- Go to Devices β Compliance β Scripts
- Click Add β Windows 10 and later
- Upload this PowerShell script
- Set Run script in 64 bit PowerShell Host to Yes
- Set Run this script using the logged on credentials to No (runs as SYSTEM)
Link to Compliance Policyβ
This script is used by the Custom Compliance Policy.
See ππͺπ»Compliance - Custom - Detect RMM for:
- The compliance rules that evaluate this script's output
- Device assignment configuration
- Actions for non-compliance
Note: This script is not assigned separately β the compliance policy handles all assignments
Pro Tips π§ β
Test Before Deployingβ
Before creating the compliance policy, test the script locally:
# Run locally on a test device
.\CCScript-Detect-RMM.ps1
# Check the JSON output
# Verify it detects your RMM correctly
Monitor False Positivesβ
If devices are failing compliance but the RMM is installed:
- Check the script's detection logic for your specific RMM
- Verify process names, service names, and paths match your installation
- Update the catalog if needed (e.g., custom install paths)
Add Custom RMM Toolsβ
If you use an RMM not in the catalog, extend the script:
# Add to the Get-RmmCatalog function
'CustomRMM' = [ordered]@{
ProcessNames = @('CustomAgent')
ServiceNamePatterns = @('CustomRMM.*')
Paths = @('C:\Program Files\CustomRMM\*')
RegDisplayNamePatterns = @('CustomRMM')
}
Then add it to the $autoOrder array.
Decompress Details for Troubleshootingβ
The detailsCompressed field contains full detection data:
# Decompress the details
$compressed = "H4sIAAAAAAAA..." # from JSON output
$bytes = [Convert]::FromBase64String($compressed)
$msIn = New-Object System.IO.MemoryStream($bytes, 0, $bytes.Length)
$gzip = New-Object System.IO.Compression.GZipStream($msIn, [IO.Compression.CompressionMode]::Decompress)
$sr = New-Object System.IO.StreamReader($gzip)
$json = $sr.ReadToEnd()
$sr.Close()
$gzip.Close()
$msIn.Close()
# View the details
$json | ConvertFrom-Json | ConvertTo-Json -Depth 10
This shows exactly which processes, services, and paths were found (or not found).
Troubleshooting π§β
Script returns present: false but RMM is installedβ
Possible causes:
- Custom installation path not in the catalog
- Service renamed during installation
- Process running under a different name
Solution:
- Run the script manually to see what's detected
- Check which detection methods are failing
- Update the catalog with your specific paths/names
Script returns active: false but service is runningβ
Possible causes:
- Service is installed but not started
- Process crashed but service is still registered
- Service startup type is "Disabled"
Solution:
- Check service status:
Get-Service -Name "YourRMMService" - Check process list:
Get-Process -Name "YourRMMProcess" - Restart the service or reboot the device
Script outputs nothingβ
Possible causes:
- Script execution policy blocked it
- Error in the script (syntax/logic)
- Output redirected or suppressed
Solution:
- Check script execution policy:
Get-ExecutionPolicy - Run manually to see error messages
- Verify
$ErrorActionPreferenceisn't hiding errors
Final Thoughts π§β
Microsoft's compliance checks cover the basics.
But they're generic. They don't know about your tools.
That's why Custom Compliance exists.
This script lets you enforce compliance based on business-critical tools like RMM agents.
Because "compliant" isn't just what Microsoft says.
It's what you say.
And if you say a device needs an RMM agent?
Then it needs an RMM agent.
<#
Intune Custom Compliance β RMM Auto Detection (Generic)
- Output: single-line JSON with top-level booleans: present, active
- Works with the provided Intune Rules that check $.present == true AND $.active == true
- Supports Auto discovery across common RMM/remote tools.
Configure (optional):
- Env var RMM_CHOICE can override selection. Use "Auto" to scan all.
#>
# Ensure no extraneous output
$InformationPreference = 'SilentlyContinue'
$ErrorActionPreference = 'SilentlyContinue'
# -------- Utilities --------
function Invoke-Safe {
param([scriptblock]$ScriptBlock)
try { & $ScriptBlock } catch { $null }
}
function Compress-StringGzipBase64 {
param([Parameter(Mandatory)][string]$InputString)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
$msOut = New-Object System.IO.MemoryStream
$gzip = New-Object System.IO.Compression.GZipStream($msOut, [IO.Compression.CompressionLevel]::Optimal)
$gzip.Write($bytes, 0, $bytes.Length)
$gzip.Close()
$b64 = [Convert]::ToBase64String($msOut.ToArray())
$msOut.Dispose()
return $b64
}
# -------- Catalog --------
function Get-RmmCatalog {
$catalog = [ordered]@{
'NinjaOne' = [ordered]@{
ProcessNames = @('NinjaRMMAgent','NinjaRMMAgentUpdater','NinjaRMMAgentSVC')
ServiceNamePatterns = @('^Ninja.*RMM.*','^NinjaRMMAgent(Updater)?$')
Paths = @('C:\Program Files\Ninja*\*','C:\Program Files\NinjaRMMAgent\*')
RegDisplayNamePatterns = @('Ninja.*RMM')
}
'DattoRMM' = [ordered]@{
ProcessNames = @('CagService','AgentService','AEMAgent','CentraStage')
ServiceNamePatterns = @('CentraStage','AEM.*Agent','CagService','Datto.*RMM')
Paths = @('C:\Program Files\CentraStage\*','C:\Program Files (x86)\CentraStage\*','C:\Program Files\Datto\RMM\*','C:\Program Files (x86)\Datto\RMM\*')
RegDisplayNamePatterns = @('Datto.*RMM','CentraStage','AEM')
}
'TacticalRMM' = [ordered]@{
ProcessNames = @('tacticalrmm','meshagent')
ServiceNamePatterns = @('tactical.*rmm','mesh(agent)?','Mesh Agent')
Paths = @('C:\Program Files\TacticalRMM\*','C:\Program Files\Mesh Agent\*','C:\Program Files (x86)\Mesh Agent\*')
RegDisplayNamePatterns = @('Tactical.*RMM','MeshCentral|Mesh Agent')
}
'ConnectWiseAutomate' = [ordered]@{
ProcessNames = @('LTService','LTSvcMon','LTClient','LTTray')
ServiceNamePatterns = @('^LTService$','^LTSvcMon$','LabTech|ConnectWise Automate')
Paths = @('C:\Program Files (x86)\LabTech\*','C:\Program Files\LabTech\*','C:\Program Files (x86)\ConnectWise Automate\*')
RegDisplayNamePatterns = @('LabTech|ConnectWise Automate')
}
'Nable' = [ordered]@{
ProcessNames = @('Agent','WinAgent','ncagent','NableTray','AMPAgent','RMMServiceHost')
ServiceNamePatterns = @('N-?able.*Agent','Ncentral.*Agent','Windows Agent Maintenance Service','Advanced Monitoring Agent','SolarWinds.*RMM')
Paths = @('C:\Program Files\N-able\*','C:\Program Files (x86)\N-able\*','C:\Program Files (x86)\SolarWinds MSP\Remote Monitoring*\*','C:\Program Files (x86)\Advanced Monitoring Agent\*')
RegDisplayNamePatterns = @('N-?able','N-central','SolarWinds.*RMM','Advanced Monitoring Agent')
}
'Atera' = [ordered]@{
ProcessNames = @('AteraAgent')
ServiceNamePatterns = @('^AteraAgent$')
Paths = @('C:\Program Files\Atera Networks\AteraAgent\*','C:\Program Files (x86)\Atera Networks\AteraAgent\*')
RegDisplayNamePatterns = @('Atera')
}
'ManageEngineEndpointCentral' = [ordered]@{
ProcessNames = @('dcagentservice','UEMSAgentService')
ServiceNamePatterns = @('ManageEngine.*Desktop Central.*Agent','Desktop Central - Agent','UEMS.*Agent')
Paths = @('C:\Program Files\DesktopCentral_Agent\*','C:\Program Files (x86)\DesktopCentral_Agent\*','C:\Program Files\ManageEngine\UEMS_Agent\*')
RegDisplayNamePatterns = @('ManageEngine.*(Desktop|Endpoint).*Agent','UEMS.*Agent')
}
'Pulseway' = [ordered]@{
ProcessNames = @('Pulseway','PCMonitorSrv')
ServiceNamePatterns = @('Pulseway','PC Monitor')
Paths = @('C:\Program Files\Pulseway\*','C:\Program Files (x86)\Pulseway\*')
RegDisplayNamePatterns = @('Pulseway','PC Monitor')
}
'Splashtop' = [ordered]@{
ProcessNames = @('SRService','Streamer','SplashtopService','SplashtopStreamer')
ServiceNamePatterns = @('Splashtop.*(Remote|Connect|Streamer).*Service','^SplashtopRemoteService$')
Paths = @('C:\Program Files (x86)\Splashtop\*','C:\Program Files\Splashtop\*')
RegDisplayNamePatterns = @('Splashtop')
}
'TeamViewer' = [ordered]@{
ProcessNames = @('TeamViewer','TeamViewer_Service')
ServiceNamePatterns = @('^TeamViewer(\d+)?$','TeamViewer.*Service')
Paths = @('C:\Program Files\TeamViewer\*')
RegDisplayNamePatterns = @('TeamViewer')
}
}
return $catalog
}
# -------- Detection Helpers --------
function Test-AnyPathExists {
param([string[]]$Paths)
$hits = @()
foreach ($p in $Paths) {
if (Invoke-Safe { Test-Path -LiteralPath $p -PathType Any }) { $hits += $p; continue }
# allow wildcards too
$items = Invoke-Safe { Get-ChildItem -Path $p -ErrorAction SilentlyContinue }
if ($items) { $hits += $p }
}
[PSCustomObject]@{ any = [bool]$hits; hits = $hits }
}
function Find-ServicesByPatterns {
param([string[]]$Patterns)
$matches = @()
$svcs = Invoke-Safe { Get-Service } | Where-Object { $_ }
if ($svcs) {
foreach ($pat in $Patterns) {
try { $regex = [System.Text.RegularExpressions.Regex]::new($pat, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) } catch { continue }
foreach ($s in $svcs) {
if ($regex.IsMatch([string]$s.Name) -or $regex.IsMatch([string]$s.DisplayName)) {
$matches += [PSCustomObject]@{
Name = $s.Name
DisplayName = $s.DisplayName
State = $s.Status.ToString()
Started = [bool]($s.Status -eq 'Running')
}
}
}
}
}
$running = $matches | Where-Object { $_.Started }
[PSCustomObject]@{
anyInstalled = [bool]$matches
anyRunning = [bool]$running
installed = $matches | Sort-Object Name -Unique
running = $running | Sort-Object Name -Unique
}
}
function Find-ProcessesByNames {
param([string[]]$Names)
$norm = @()
foreach ($n in $Names) {
if ([string]::IsNullOrWhiteSpace($n)) { continue }
$norm += ($n -replace '\.exe$','')
}
$procs = Invoke-Safe { Get-Process } | Where-Object { $_ }
$hits = @()
if ($procs) {
foreach ($p in $procs) {
if ($norm -contains $p.Name) { $hits += $p }
}
}
[PSCustomObject]@{
any = [bool]$hits
running = $hits | Select-Object -Property Name,Id,StartTime -ErrorAction SilentlyContinue
}
}
function Find-UninstallMatches {
param([string[]]$Patterns)
$roots = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
$matches = @()
foreach ($r in $roots) {
$items = Invoke-Safe { Get-ChildItem $r -ErrorAction Stop }
foreach ($i in ($items | Where-Object { $_ })) {
$p = $i.PSPath
$dn = Invoke-Safe { (Get-ItemProperty $p -ErrorAction Stop).DisplayName }
$dv = Invoke-Safe { (Get-ItemProperty $p -ErrorAction Stop).DisplayVersion }
if (-not $dn) { continue }
foreach ($pat in $Patterns) {
try { $regex = [System.Text.RegularExpressions.Regex]::new($pat, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) } catch { continue }
if ($regex.IsMatch([string]$dn)) {
$matches += [PSCustomObject]@{ Key=$i.PSChildName; DisplayName=$dn; DisplayVersion=$dv }
}
}
}
}
[PSCustomObject]@{ any = [bool]$matches; items = $matches | Sort-Object DisplayName -Unique }
}
function Get-FileVersionFromPaths {
param([string[]]$Paths)
$versions = @()
foreach ($p in $Paths) {
$files = Invoke-Safe { Get-ChildItem -Path $p -Include *.exe -Recurse -ErrorAction SilentlyContinue }
foreach ($f in ($files | Where-Object { $_ })) {
$fv = Invoke-Safe { (Get-Item $f.FullName).VersionInfo.FileVersion }
if ($fv) { $versions += [PSCustomObject]@{ File=$f.FullName; Version=$fv } }
}
}
$versions | Sort-Object Version -Descending | Select-Object -First 1
}
function Test-RmmDefinition {
param([hashtable]$Def,[string]$Key)
$pathRes = Test-AnyPathExists -Paths $Def.Paths
$svcRes = Find-ServicesByPatterns -Patterns $Def.ServiceNamePatterns
$procRes = Find-ProcessesByNames -Names $Def.ProcessNames
$regRes = Find-UninstallMatches -Patterns $Def.RegDisplayNamePatterns
$verFile = Get-FileVersionFromPaths -Paths $Def.Paths
$present = ($pathRes.any -or $svcRes.anyInstalled -or $regRes.any)
$active = ($svcRes.anyRunning -or $procRes.any)
$version = $null
if ($regRes.items) { $version = ($regRes.items | Select-Object -ExpandProperty DisplayVersion -First 1) }
if (-not $version -and $verFile) { $version = $verFile.Version }
$details = [ordered]@{
key = $Key
pathsChecked = $Def.Paths
servicesMatched = $svcRes.installed
servicesRunning = $svcRes.running
processesRunning = $procRes.running
regMatches = $regRes.items
topExeVersion = $verFile
}
[PSCustomObject]@{
key = $Key
present = [bool]$present
active = [bool]$active
pathExists= [bool]$pathRes.any
serviceInstalled = [bool]$svcRes.anyInstalled
serviceRunning = [bool]$svcRes.anyRunning
processRunning = [bool]$procRes.any
regInstalled = [bool]$regRes.any
version = $version
details = $details
}
}
# -------- MAIN --------
$RmmChoice = $env:RMM_CHOICE
if ([string]::IsNullOrWhiteSpace($RmmChoice)) { $RmmChoice = 'Auto' }
$catalog = Get-RmmCatalog
# Preferred auto order: true RMMs first, then remote-only tools
$autoOrder = @('NinjaOne','DattoRMM','TacticalRMM','ConnectWiseAutomate','Nable','Atera','ManageEngineEndpointCentral','Pulseway','Splashtop','TeamViewer')
$final = $null
$selectedKey = $RmmChoice
if ($RmmChoice -ieq 'Auto') {
foreach ($k in $autoOrder) {
if (-not $catalog.ContainsKey($k)) { continue }
$res = Test-RmmDefinition -Def $catalog[$k] -Key $k
if ($res.present) { $final = $res; $selectedKey = $k; break }
}
if (-not $final) {
# none detected
$final = [PSCustomObject]@{
key=$selectedKey; present=$false; active=$false;
pathExists=$false; serviceInstalled=$false; serviceRunning=$false; processRunning=$false; regInstalled=$false; version=$null; details=@{}
}
}
} else {
if ($catalog.ContainsKey($selectedKey)) {
$final = Test-RmmDefinition -Def $catalog[$selectedKey] -Key $selectedKey
} else {
$final = [PSCustomObject]@{
key=$selectedKey; present=$false; active=$false;
pathExists=$false; serviceInstalled=$false; serviceRunning=$false; processRunning=$false; regInstalled=$false; version=$null; details=@{error="Unknown RMM choice"}
}
}
}
# Also-detected summary (optional)
$also = @()
foreach ($k in $catalog.Keys) {
if ($k -eq $final.key) { continue }
$r = Test-RmmDefinition -Def $catalog[$k] -Key $k
if ($r.present) { $also += [PSCustomObject]@{ key=$k; active=$r.active } }
}
# Build output JSON
$now = [DateTime]::UtcNow.ToString('o')
$detailsJson = ($final.details | ConvertTo-Json -Depth 6)
$compressed = Compress-StringGzipBase64 -InputString $detailsJson
$out = [ordered]@{
tool = $final.key
present = [bool]$final.present
active = [bool]$final.active
serviceInstalled = [bool]$final.serviceInstalled
serviceRunning = [bool]$final.serviceRunning
processRunning = [bool]$final.processRunning
pathExists= [bool]$final.pathExists
regInstalled = [bool]$final.regInstalled
version = $final.version
alsoDetected = $also
timestampUtc = $now
detailsCompressed = $compressed
}
$outJson = $out | ConvertTo-Json -Depth 8 -Compress
$outJson