
Get All Devices with a Piece of Software Installed
This script finds all devices managed by NinjaOne that have a specific piece of software installed (defaults to Google Chrome)
# --------------------------------------------------
# Author: Gavin Stone (NinjaOne)
# Attribution: Luke Whitelock (NinjaOne) for his work on the Authentication Functions
# Date: 2026-03-10
# Description: Find all devices managed by NinjaOne that have Google Chrome installed. Return the device name, OS, and the Chrome version found.
# Version: 1.0
# --------------------------------------------------
# User editable variables:
$NinjaOneInstance = 'eu.ninjarmm.com' # Please replace with the region instance you login to (app.ninjarmm.com, us2.ninjarmm.com, eu.ninjarmm.com, ca.ninjarmm.com, oc.ninjarmm.com)
$NinjaOneClientId = ''
$NinjaOneClientSecret = ''
# Regex pattern to match software names against (e.g. 'Google Chrome', 'Firefox')
$SoftwareNameFilter = 'Google Chrome'
# Functions for Authentication
function Get-NinjaOneToken {
[CmdletBinding()]
param()
if ($Script:NinjaOneInstance -and $Script:NinjaOneClientID -and $Script:NinjaOneClientSecret ) {
if ($Script:NinjaTokenExpiry -and (Get-Date) -lt $Script:NinjaTokenExpiry) {
return $Script:NinjaToken
}
else {
if ($Script:NinjaOneRefreshToken) {
$Body = @{
'grant_type' = 'refresh_token'
'client_id' = $Script:NinjaOneClientID
'client_secret' = $Script:NinjaOneClientSecret
'refresh_token' = $Script:NinjaOneRefreshToken
}
}
else {
$body = @{
grant_type = 'client_credentials'
client_id = $Script:NinjaOneClientID
client_secret = $Script:NinjaOneClientSecret
scope = 'monitoring management'
}
}
$token = Invoke-RestMethod -Uri "https://$($Script:NinjaOneInstance -replace '/ws','')/ws/oauth/token" -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -UseBasicParsing
$Script:NinjaTokenExpiry = (Get-Date).AddSeconds($Token.expires_in)
$Script:NinjaToken = $token
Write-Host 'Fetched New Token'
return $token
}
}
else {
Throw 'Please run Connect-NinjaOne first'
}
}
function Connect-NinjaOne {
[CmdletBinding()]
param (
[Parameter(mandatory = $true)]
$NinjaOneInstance,
[Parameter(mandatory = $true)]
$NinjaOneClientID,
[Parameter(mandatory = $true)]
$NinjaOneClientSecret,
$NinjaOneRefreshToken
)
$Script:NinjaOneInstance = $NinjaOneInstance
$Script:NinjaOneClientID = $NinjaOneClientID
$Script:NinjaOneClientSecret = $NinjaOneClientSecret
$Script:NinjaOneRefreshToken = $NinjaOneRefreshToken
try {
$Null = Get-NinjaOneToken -ea Stop
}
catch {
Throw "Failed to Connect to NinjaOne: $_"
}
}
function Invoke-NinjaOneRequest {
param(
$Method,
$Body,
$InputObject,
$Path,
$QueryParams,
[Switch]$Paginate,
[Switch]$AsArray
)
$Token = Get-NinjaOneToken
if ($InputObject) {
if ($AsArray) {
$Body = $InputObject | ConvertTo-Json -depth 100
if (($InputObject | Measure-Object).count -eq 1 ) {
$Body = '[' + $Body + ']'
}
}
else {
$Body = $InputObject | ConvertTo-Json -depth 100
}
}
try {
if ($Method -in @('GET', 'DELETE')) {
if ($Paginate) {
$After = 0
$PageSize = 1000
$NinjaResult = do {
$Result = Invoke-WebRequest -uri "https://$($Script:NinjaOneInstance)/api/v2/$($Path)?pageSize=$PageSize&after=$After$(if ($QueryParams){"&$QueryParams"})" -Method $Method -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' -UseBasicParsing
$Result
$ResultCount = ($Result.id | Measure-Object -Maximum)
$After = $ResultCount.maximum
} while ($ResultCount.count -eq $PageSize)
}
else {
$NinjaResult = Invoke-WebRequest -uri "https://$($Script:NinjaOneInstance)/api/v2/$($Path)$(if ($QueryParams){"?$QueryParams"})" -Method $Method -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -UseBasicParsing
}
}
elseif ($Method -in @('PATCH', 'PUT', 'POST')) {
$NinjaResult = Invoke-WebRequest -uri "https://$($Script:NinjaOneInstance)/api/v2/$($Path)$(if ($QueryParams){"?$QueryParams"})" -Method $Method -Headers @{Authorization = "Bearer $($token.access_token)" } -Body $Body -ContentType 'application/json; charset=utf-8' -UseBasicParsing
}
else {
Throw 'Unknown Method'
}
}
catch {
Throw "Error Occured: $_"
}
try {
return $NinjaResult.content | ConvertFrom-Json -ea stop
}
catch {
return $NinjaResult.content
}
}
# Connect to NinjaOne API
try {
Connect-NinjaOne -NinjaOneInstance $NinjaOneInstance -NinjaOneClientID $NinjaOneClientId -NinjaOneClientSecret $NinjaOneClientSecret
}
catch {
Write-Output "Failed to connect to NinjaOne API: $_"
exit 1
}
# ─── Script Logic ─────────────────────────────────────────────────────────────
# Query all software inventory in bulk using cursor pagination
Write-Host "Fetching software inventory..."
$AllSoftware = [System.Collections.Generic.List[PSCustomObject]]::new()
$CursorName = $null
$PageSize = 1000
do {
$QueryParams = "pageSize=$PageSize"
if ($CursorName) {
$QueryParams += "&cursor=$CursorName"
}
$Response = Invoke-NinjaOneRequest -Method GET -Path 'queries/software' -QueryParams $QueryParams
if ($Response.results) {
foreach ($Item in $Response.results) {
$AllSoftware.Add($Item)
}
}
$CursorName = $Response.cursor.name
$PageCount = if ($Response.results) { $Response.results.Count } else { 0 }
Write-Host " Retrieved $($AllSoftware.Count) records so far..."
} while ($CursorName -and $PageCount -eq $PageSize)
Write-Host "Retrieved $($AllSoftware.Count) total software record(s)"
# Filter to matching software
$MatchingSoftware = $AllSoftware | Where-Object { $_.name -match $SoftwareNameFilter }
if ($MatchingSoftware.Count -eq 0) {
Write-Host "No installations found matching '$SoftwareNameFilter'."
exit 0
}
# Get unique device IDs that have the software
$DeviceIds = $MatchingSoftware | Select-Object -ExpandProperty deviceId -Unique
Write-Host "Found $($MatchingSoftware.Count) installation(s) across $($DeviceIds.Count) device(s). Fetching device details..."
# Fetch all devices to resolve names (single paginated call)
$AllDevices = Invoke-NinjaOneRequest -Method GET -Path 'devices' -Paginate
$DeviceLookup = @{}
foreach ($Device in $AllDevices) {
$DeviceLookup[$Device.id] = $Device
}
# Build results
$Results = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($App in $MatchingSoftware) {
$Device = $DeviceLookup[$App.deviceId]
$Results.Add([PSCustomObject]@{
DeviceName = if ($Device) { $Device.systemName } else { "Unknown (ID: $($App.deviceId))" }
DeviceId = $App.deviceId
DeviceClass = if ($Device) { $Device.nodeClass } else { 'N/A' }
SoftwareName = $App.name
Version = $App.version
Publisher = $App.publisher
InstallDate = $App.installDate
})
}
Write-Host "`nFound $($Results.Count) installation(s) across $($DeviceIds.Count) device(s)."
$Results | Format-Table -AutoSize
# Uncomment the next line to export to a CSV
$Results | Export-Csv -Path ('C:\Temp\NinjaOneSoftwareReport_' + (Get-Date -f "yyyyMMdd_HHmm") + '.csv') -NoTypeInformation
Get-DevicesWithSoftware
Overview
This script finds all devices managed by NinjaOne that have a specific piece of software installed (defaults to Google Chrome). It uses the bulk software inventory query endpoint for efficient retrieval across the entire estate, then resolves device names from the devices endpoint.
Attribution
- Author: Gavin Stone (NinjaOne)
Requirements / Prerequisites
- NinjaOne API Credentials:
NinjaOneClientIdNinjaOneClientSecret
- NinjaOne Instance URL: e.g.,
eu.ninjarmm.com
How It Works
- Authentication: The script authenticates with the NinjaOne API using OAuth2 Client Credentials, obtaining an access token via
Connect-NinjaOne. - Bulk Software Query: Retrieves the entire software inventory via
queries/softwareusing cursor-based pagination, collecting all records in a single bulk operation rather than querying each device individually. - Filter Matches: Filters the software list using the
$SoftwareNameFilterregex pattern (default:Google Chrome). - Resolve Device Names: Fetches all devices via
devicesto build a lookup table, mapping device IDs from the software results to device names and classes. - Output Results: Displays a summary count and a formatted table of all matching installations with device name, version, publisher, and install date.
Usage
-
Set Variables:
-
Open
Get-DevicesWithSoftware.ps1in an editor (e.g., VS Code, PowerShell ISE). -
Fill in your NinjaOne credentials and any script-specific variables at the top:
$NinjaOneInstance = "eu.ninjarmm.com" $NinjaOneClientId = "your_client_id" $NinjaOneClientSecret = "your_client_secret" $SoftwareNameFilter = 'Google Chrome'
-
-
Run the Script:
.\Get-DevicesWithSoftware.ps1
Expected Output
Fetching software inventory...
Retrieved 1000 records so far...
Retrieved 2000 records so far...
Retrieved 2420 records so far...
Retrieved 2420 total software record(s)
Found 12 installation(s) across 12 device(s). Fetching device details...
Found 12 installation(s) across 12 device(s).
DeviceName DeviceId DeviceClass SoftwareName Version Publisher InstallDate
---------- -------- ----------- ------------ ------- --------- -----------
DESK-001 142 WINDOWS_WORKSTATION Google Chrome 133.0.6943.142 Google LLC 2025-01-15
DESK-002 287 WINDOWS_WORKSTATION Google Chrome 133.0.6943.142 Google LLC 2025-02-01
SRV-WEB-01 403 WINDOWS_SERVER Google Chrome 133.0.6943.142 Google LLC 2024-11-20
LAPTOP-JS 891 WINDOWS_WORKSTATION Google Chrome 132.0.6834.83 Google LLC 2025-01-10
Troubleshooting
-
Issue: Authentication fails with an error message.
- Solution: Verify that
$NinjaOneClientIdand$NinjaOneClientSecretare correct and that the API client has the required scopes (monitoring,management).
- Solution: Verify that
-
Issue: The script cannot connect to the NinjaOne API.
- Solution: Ensure that
$NinjaOneInstanceis correct (e.g.,eu.ninjarmm.com,app.ninjarmm.com,oc.ninjarmm.com) and accessible from your network.
- Solution: Ensure that
-
Issue: No results returned but the software is known to be installed.
- Solution: Check that
$SoftwareNameFiltermatches the display name shown in NinjaOne. The filter uses regex — special characters like+or.may need escaping (e.g.\.NETinstead of.NET).
- Solution: Check that
Notes
- Ensure that your NinjaOne API credentials are kept secure and not shared.
- This script uses the bulk
queries/softwareendpoint which retrieves the entire software inventory in just 2 paginated API calls (software + devices) — much faster than the per-device approach which requires N+1 calls. - The
queries/softwareendpoint uses named cursor pagination (different from theafter-based pagination used bydevices), which is handled directly in the script. - Change
$SoftwareNameFilterto search for any software — e.g.'Firefox','7-Zip','Microsoft Teams'. - Uncomment the last line of the script to export results to a CSV file.