Skip to main content

📚🪟💻📜CCScript - Windows - Detect RMM

📚🪟💻📜CCScript - Windows - Detect RMM.ps1

<#
Intune Custom ComplianceRMM 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