Skip to main content

NinjaOne custom fields - exploring endless possibilities

· 10 min read

This post uses code from CyberDrain

This post draws on multiple posts, click the link below to check out CyberDrain.com and support Kelvin's fantastic work for the MSP community.

Custom fields are a great way to store arbitrary data from your devices in NinjaOne. In this post I will explore a few examples, some using code from CyberDrain, which store data in NinjaOne custom fields.

This post was updated on 2022/12/22 to add a new script to run a speedtest on a device and store the results in NinjaOne.

Battery Health

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding seven role custom fields for devices with the Windows Laptop role:

Field NameField TypeDescription
Has BatteriesCheckboxWhether the device has batteries
Battery IdentifierTextThe ID of the battery being reported on.
Battery Design CapacityIntegerThe original design capacity of the laptop's battery.
Battery Full Charge CapacityIntegerThe current fully charged capacity of the laptop's battery.
Battery Health PercentIntegerThe current percentage of battery health.
Battery Cycle CountIntegerThe number of times the battery has been cycled. That is drained fully and charged.
Additional BatteryCheckboxThe system has an additional battery, only the first is reported in the fields above.

The Script

Working on it

Ninja have confirmed that they are working on native functionality which will replace this script. Stay tuned to Ninja's release notes on the NinjaOne Dojo for more information.

This Script Has Been Updated

This script was updated after being published, if you're using it please compare the version you have with the version available here.

$OutputPath = 'C:\RMM\Data'

if (-not (Test-Path -Path $OutputPath)) {
New-Item -Type Directory -Path $OutputPath | Out-Null
}

& powercfg /batteryreport /XML /OUTPUT "$OutputPath\batteryreport.xml" | Out-Null

[xml]$Report = Get-Content "$OutputPath\batteryreport.xml"

$BatteryStatus = $Report.BatteryReport.Batteries.Battery | ForEach-Object {
[PSCustomObject]@{
DesignCapacity = [int]$_.DesignCapacity
FullChargeCapacity = [int]$_.FullChargeCapacity
CycleCount = [int]$_.CycleCount
Id = $_.id
}
}

if (!$BatteryStatus) {
Ninja-Property-Set hasBatteries false | Out-Null
Write-Output "No batteries found."
} else {
Ninja-Property-Set hasBatteries true | Out-Null
}

$Battery = @{}

if ($BatteryStatus.Count -gt 1) {
Ninja-Property-Set additionalBattery true | Out-Null
$Battery = $BatteryStatus[0]
Write-Output "More than 1 battery found."
} elseif ($BatteryStatus.Count -eq 1) {
Ninja-Property-Set additionalBattery false | Out-Null
Write-Output "One battery found."
$Battery = $BatteryStatus[0]
} elseif ($BatteryStatus.Id) {
Ninja-Property-Set additionalBattery false | Out-Null
$Battery = $BatteryStatus
}

if ($Battery) {
Write-Output "Setting NinjaOne custom fields for first battery."

Ninja-Property-Set batteryIdentifier $Battery.id | Out-Null
Ninja-Property-Set batteryDesignCapacity $Battery.DesignCapacity | Out-Null
Ninja-Property-Set batteryFullChargeCapacity $Battery.FullChargeCapacity | Out-Null

[int]$HealthPercent = ([int]$Battery.FullChargeCapacity / [int]$Battery.DesignCapacity) * 100

Ninja-Property-Set batteryHealthPercent $HealthPercent | Out-Null
Ninja-Property-Set batteryCycleCount $Battery.CycleCount | Out-Null
} else {
Write-Output "Failed to parse battery status correctly."
}

Source: https://www.cyberdrain.com/monitoring-with-powershell-monitoring-battery-health/

The Results

Battery Related Custom Fields

You can set this up to run on a schedule - we run this script monthly to keep up-to-date battery information in NinjaOne.

Domain Join Status

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding three role custom fields for devices with the Windows Laptop role:

Field NameField TypeDescription
Domain Join StatusDrop-downThe current domain join status of the device.
Domain NameTextThe name of the currently joined AD domain.
Tenant NameTextThe name of the currently joined Azure AD tenant.

The domain join status field should be configured with the following option values:

  • Azure AD Domain
  • AD Domain
  • Hybrid Azure AD/AD Domain
  • On-premise DRS
  • No Domain

Options

We've created a dropdown field for this script, we can't set the options by name with these - so first we need to find out how NinjaOne wants us to give the selected option. To do that we're going to open a PowerShell session to any device in the role that has the field assigned and run:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
Import-Module NJCliPSh
Ninja-Property-Options domainJoinStatus

This will print a list that looks something like this:

1989092a-1ae0-4e6e-b274-50ea9618d436=Azure AD Domain
53745d33-fd5e-4c7d-bfd5-fea174a46781=No Domain
bb4c5215-ead7-47a3-8f36-f711c81580ce=On-premises DRS
c23486e9-49eb-4acb-8f9d-7f5d8711c284=AD Domain
c6e7ef80-68a1-4591-8055-5cee585f4f79=Hybrid Azure AD/AD Domain

When we want to pass the option to the field we need to use the GUID - for example 1989092a-1ae0-4e6e-b274-50ea9618d436 for Azure AD Domain.

The Script

This Script Requires Input

This script requires user input, whether in the form of variables, parameters or edits to the script itself before you can run it. Areas where you need to provide input will be indicated with:

### Inline Comments

and / or

'<MARKED STRINGS>'

Parameters will be indicated before the script block.

This Script Has Been Updated

This script was updated after being published, if you're using it please compare the version you have with the version available here.

$DSRegOutput = [PSObject]::New()

& dsregcmd.exe /status | Where { $_ -match ' : ' } | ForEach {
$Item = $_.Trim() -split '\s:\s'
$DSRegOutput | Add-Member -MemberType NoteProperty -Name $($Item[0] -replace '[:\s]', '') -Value $Item[1] -ErrorAction SilentlyContinue
}

if ($DSRegOutput.DomainName) {
$domainName = $DSRegOutput.DomainName
}

if ($DSRegOutput.AzureTenantName -or $DSRegOutput.TenantName) {
$tenantName = $DSRegOutput.TenantName
}

if ($DSRegOutput.AzureADJoined -eq 'YES') {
if ($DSRegOutput.DomainJoined -eq 'YES') {
Ninja-Property-Set domainJoinStatus '<GUID FOR HYBRID>' | Out-Null
Ninja-Property-Set domainName $domainName
Ninja-Property-Set tenantName $tenantName
} else {
Ninja-Property-Set domainJoinStatus '<GUID FOR AZURE AD>' | Out-Null
Ninja-Property-Set tenantName $tenantName
}
} elseif ($DSRegOutput.DomainJoined -eq 'YES') {
Ninja-Property-Set domainJoinStatus '<GUID FOR AD>' | Out-Null
Ninja-Property-Set domainName $domainName
} elseif ($DSRegOutput.EnterpriseJoined -eq 'YES') {
Ninja-Property-Set domainJoinStatus '<GUID FOR DRS>' | Out-Null
Ninja-Property-Set domainName $domainName
} else {
Ninja-Property-Set domainJoinStatus '<GUID FOR NO DOMAIN>' | Out-Null
}

The Results

Domain Related Custom Fields

You can set this up to run on a schedule - we run this script monthly to keep up-to-date domain information in NinjaOne.

Autopilot Hardware Identifier

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

"Field length"

Make sure when creating the custom field below that you set the character limit for the field to allow over 4000 characters using the "Advanced Settings" link on the field creation/edit page.

Advanced Settings Character Limit

We're adding one role custom field for devices with the Windows Desktops and Laptops role:

Field NameField TypeDescription
Autopilot HWIDSecureThe autopilot hardware identifier.

The Script

$DeviceDetailParams = @{
Namespace = 'root/cimv2/mdm/dmmap'
Class = 'MDM_DevDetail_Ext01'
Filter = "InstanceID='Ext' AND ParentID='./DevDetail'"
}

$DeviceDetail = (Get-CimInstance @DeviceDetailParams)
if ($DeviceDetail) {
$Hash = $DeviceDetail.DeviceHardwareData
} else {
Throw 'Unable to retrieve device details.'
}

Ninja-Property-Set autopilotHWID $Hash

The Results

HWID Related Custom Fields

We run this after hardware changes or post onboarding.

Wireless LAN Monitoring

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding three role custom fields for devices with the Windows Desktops and Laptops role:

Field NameField TypeDescription
WLAN Disconnect ReasonsMulti-lineA list of the WLAN disconnect reasons with frequency. JSON
WLAN FailuresIntegerThe number of WLAN failures in the report.
WLAN WarningsIntegerThe number of WLAN warnings in the report.

The Script

try {
$WLANReport = &netsh wlan show wlanreport
$WriteTime = Get-Item "$($ENV:SystemDrive)\ProgramData\Microsoft\Windows\WLANReport\wlan-report-latest.xml" | Where-Object {
$_.LastWriteTime -gt (Get-Date).AddHours(-4)
}
if (!$WriteTime) { throw 'No recent WLAN report found.' }
$WLANSummary = $WriteTime | Select-XML -XPath '//WlanEventsSummary' | Select-Object -ExpandProperty 'Node'
$Failures = $WLANSummary.StatusSummary.Failed
$Successes = $WLANSummary.StatusSummary.Successful
$Warnings = $WLANSummary.StatusSummary.Warning
$DisconnectReasons = $WLANSummary.DisconnectReasons.Reason
if ($DisconnectReasons) {
$ReasonOutput = $DisconnectReasons | ForEach-Object {
[PSCustomObject]@{
Reason = $_.message
Count = [int]$_.count
Type = $_.type
}
}
}

Ninja-Property-Set wlanFailures $Failures | Out-Null
Ninja-Property-Set wlanWarnings $Warnings | Out-Null
Ninja-Property-Set wlanDisconnectReasons ($ReasonOutput | ConvertTo-Json) | Out-Null
} catch {
Write-Error "Failed to generate, retrieve or parse WLAN report: $($_.Exception.Message)"
}

The Results

WLAN Related Custom Fields

You can set this up to run on a schedule - we run this script twice daily.

Windows 11 Readiness

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding three role custom fields for devices with the Windows Desktops and Laptops role:

Field NameField TypeDescription
Windows 11 CapableCheckboxWhether the device is Windows 11 capable as determined by our checks.
Windows 11 Check DetailsMulti-lineThe details checked to determine Windows 11 compatibility. JSON

The Script

This Script Has Been Updated

This script was updated after being published, if you're using it please compare the version you have with the version available here.

try {
[int]$MinOSDiskSizeGB = 64
[int]$MinMemoryGB = 4
[Uint32]$MinClockSpeedMHz = 1000
[Uint32]$MinLogicalCores = 2
[Uint16]$RequiredAddressWidth = 64

$CPUFamilyType = @"
using Microsoft.Win32;
using System;
using System.Runtime.InteropServices;

public class CpuFamilyResult
{
public bool IsValid { get; set; }
public string Message { get; set; }
}

public class CpuFamily
{
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO
{
public ushort ProcessorArchitecture;
ushort Reserved;
public uint PageSize;
public IntPtr MinimumApplicationAddress;
public IntPtr MaximumApplicationAddress;
public IntPtr ActiveProcessorMask;
public uint NumberOfProcessors;
public uint ProcessorType;
public uint AllocationGranularity;
public ushort ProcessorLevel;
public ushort ProcessorRevision;
}

[DllImport("kernel32.dll")]
internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo);

public enum ProcessorFeature : uint
{
ARM_SUPPORTED_INSTRUCTIONS = 34
}

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature);

private const ushort PROCESSOR_ARCHITECTURE_X86 = 0;
private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12;
private const ushort PROCESSOR_ARCHITECTURE_X64 = 9;

private const string INTEL_MANUFACTURER = "GenuineIntel";
private const string AMD_MANUFACTURER = "AuthenticAMD";
private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc";

public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture)
{
CpuFamilyResult cpuFamilyResult = new CpuFamilyResult();

if (string.IsNullOrWhiteSpace(manufacturer))
{
cpuFamilyResult.IsValid = false;
cpuFamilyResult.Message = "Manufacturer is null or empty";
return cpuFamilyResult;
}

string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0";
SYSTEM_INFO sysInfo = new SYSTEM_INFO();
GetNativeSystemInfo(ref sysInfo);

switch (processorArchitecture)
{
case PROCESSOR_ARCHITECTURE_ARM64:

if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
{
bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS);

if (!isArmv81Supported)
{
string registryName = "CP 4030";
long registryValue = (long)Registry.GetValue(registryPath, registryName, -1);
long atomicResult = (registryValue >> 20) & 0xF;

if (atomicResult >= 2)
{
isArmv81Supported = true;
}
}

cpuFamilyResult.IsValid = isArmv81Supported;
cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction";
}
else
{
cpuFamilyResult.IsValid = false;
cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11";
}

break;

case PROCESSOR_ARCHITECTURE_X64:
case PROCESSOR_ARCHITECTURE_X86:

int cpuFamily = sysInfo.ProcessorLevel;
int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF;
int cpuStepping = sysInfo.ProcessorRevision & 0xFF;

if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
{
try
{
cpuFamilyResult.IsValid = true;
cpuFamilyResult.Message = "";

if (cpuFamily >= 6 && cpuModel <= 95 && !(cpuFamily == 6 && cpuModel == 85))
{
cpuFamilyResult.IsValid = false;
cpuFamilyResult.Message = "";
}
else if (cpuFamily == 6 && (cpuModel == 142 || cpuModel == 158) && cpuStepping == 9)
{
string registryName = "Platform Specific Field 1";
int registryValue = (int)Registry.GetValue(registryPath, registryName, -1);

if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8))
{
cpuFamilyResult.IsValid = false;
}
cpuFamilyResult.Message = "PlatformId " + registryValue;
}
}
catch (Exception ex)
{
cpuFamilyResult.IsValid = false;
cpuFamilyResult.Message = "Exception:" + ex.GetType().Name;
}
}
else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
{
cpuFamilyResult.IsValid = true;
cpuFamilyResult.Message = "";

if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17)))
{
cpuFamilyResult.IsValid = false;
}
}
else
{
cpuFamilyResult.IsValid = false;
cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision;
}

break;

default:
cpuFamilyResult.IsValid = false;
cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision;
break;
}
return cpuFamilyResult;
}
}
"@


$OSDriveSize = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($ENV:SystemDrive)'" | Select-Object @{
Name = "SizeGB"
Expression = { $_.Size / 1GB -as [int] }
}

if ($OSDriveSize.SizeGB -ge $MinOSDiskSizeGB) {
$OSDriveSizeSuitable = 'Yes'
} else {
$OSDriveSizeSuitable = 'No'
}

$MemorySize = Get-CimInstance -Class Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{
Name = "SizeGB"
Expression = { $_.Sum / 1GB -as [int] }
}

if ($MemorySize.SizeGB -ge $MinMemoryGB) {
$MemorySizeSuitable = 'Yes'
} else {
$MemorySizeSuitable = 'No'
}

$TPM = Get-Tpm

if ($TPM.TpmPresent) {
$TPMVersion = Get-CimInstance -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion
$TPMMajorVersion = $TPMVersion.SpecVersion.Split(",")[0]
if ($TPMMajorVersion -ge 2) {
$TPMSuitable = 'Yes'
} else {
$TPMSuitable = 'No'
}
}

$ProcessorInformation = Get-CimInstance -ClassName Win32_Processor | Select-Object -Property [a-z]*

$ProcessorAddressWidth = $ProcessorInformation.AddressWidth
$ProcessorMaxClockSpeed = $ProcessorInformation.MaxClockSpeed
$ProcessorNumberOfLogicalProcessors = $ProcessorInformation.NumberOfLogicalProcessors
$ProcessorManufacturer = $ProcessorInformation.Manufacturer
$ProcessorArchitecture = $ProcessorInformation.Architecture
$ProcessorFamily = $ProcessorInformation.Caption

Add-Type -TypeDefinition $CPUFamilyType

$CPUFamilyResult = [CpuFamily]::Validate([String]$ProcessorManufacturer, [String]$ProcessorArchitecture)

$CPUIsSuitable = $CPUFamilyResult.IsValid -and ($ProcessorAddressWidth -ge $MinAddressWidth) -and ($ProcessorMaxClockSpeed -ge $MinClockSpeedMHz) -and ($ProcessorNumberOfLogicalProcessors -ge $MinLogicalProcessors)

if (-not($CPUIsSuitable)) {
$SupportedDevices = @('Surface Studio 2', 'Precision 5520')
$SystemInfo = @(Get-CimInstance -Class Win32_ComputerSystem)[0]

if ($null -ne $ProcessorInformation) {
if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz'){
$modelOrSKUCheckLog = $systemInfo.Model.Trim()
if ($supportedDevices -contains $modelOrSKUCheckLog){
$CPUIsSuitable = $true
}
}
}
}

if ($CPUIsSuitable) {
$CPUSuitable = 'Yes'
} else {
$CPUSuitable = 'No'
}

try {
$SecureBootEnabled = Confirm-SecureBootUEFI
} catch [System.PlatformNotSupportedException] {
$SecureBootEnabled = $False
} catch [System.UnauthorizedAccessException] {
$SecureBootEnabled = $False
} catch {
$SecureBootEnabled = $False
}

if ($SecureBootEnabled) {
$SecureBootSuitable = 'Yes'
} else {
$SecureBootSuitable = 'No'
}

if ($OSDriveSizeSuitable -eq 'Yes' -and $MemorySizeSuitable -eq 'Yes' -and $CPUSuitable -eq 'Yes' -and $TPMSuitable -eq 'Yes' -and $SecureBootSuitable -eq 'Yes') {
Ninja-Property-Set windows11Capable 'Yes' | Out-Null
} else {
Ninja-Property-Set windows11Capable 'No' | Out-Null
}

$Windows11CheckDetails = @{
'OSDriveSizeSuitable' = $OSDriveSizeSuitable
'OSDriveSize' = $OSDriveSize.SizeGB
'MemorySizeSuitable' = $MemorySizeSuitable
'MemorySize' = $MemorySize.SizeGB
'CPUSuitable' = $CPUSuitable
'CPUFamily' = $ProcessorFamily
'TPMSuitable' = $TPMSuitable
'SecureBootSuitable' = $SecureBootSuitable
}

$DetailJson = $Windows11CheckDetails | ConvertTo-Json

Ninja-Property-Set windows11CheckDetails $DetailJson | Out-Null

} catch {
Write-Error $_
}

Source: https://techcommunity.microsoft.com/t5/microsoft-endpoint-manager-blog/understanding-readiness-for-windows-11-with-microsoft-endpoint/ba-p/2770866

Thanks to @gavsto for invaluable input on a couple of the checks in here - he's helped make them more robust and accurate.

The Results

Windows 11 Related Custom Fields

We ran this as a one-off job to evaluate our managed estate.

Limitations

This script won't detect compatible devices if the TPM is not enabled in the BIOS. It may not detect all compatible devices.

Windows OS Support Status

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding two role custom fields for devices with the Windows Desktops and Laptops and the Windows Server role:

Field NameField TypeDescription
Windows Active SupportCheckboxWhether the OS version is supported for feature / quality updates.
Windows Security SupportCheckboxWhether the OS version is supported for security updates.

The Script

This Script Was Updated

This script was updated after being published, if you're using it please compare the version you have with the version available here.

This script was last updated on 2023/01/13.

$TLS12Protocol = [System.Net.SecurityProtocolType] 'Ssl3 , Tls12'
[System.Net.ServicePointManager]::SecurityProtocol = $TLS12Protocol
$EndOfLifeUriWindows = 'https://endoflife.date/api/windows.json'
$EndOfLifeUriServer = 'https://endoflife.date/api/windowsserver.json'
$EoLRequestParams = @{
Method = 'GET'
}
$ProductName = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductName).ProductName
if ($ProductName -like '*Home' -or $ProductName -like '*Pro') {
$Edition = '(W)'
} else {
$Edition = '(E)'
}
if ($ProductName -like '*Server*') {
$EoLRequestParams.Uri = $EndOfLifeUriServer
$IsServerOS = $True
} else {
$EoLRequestParams.Uri = $EndOfLifeUriWindows
}
$LifeCycles = Invoke-RestMethod @EoLRequestParams
$WindowsVersion = [System.Environment]::OSVersion.Version
$OSVersion = ($WindowsVersion.Major, $WindowsVersion.Minor, $WindowsVersion.Build -Join '.')
$LifeCycle = $LifeCycles | Where-Object { $_.buildID -eq $OSVersion -and (($_.cycle -like "*$Edition*") -or ($IsServerOS)) }
if ($LifeCycle) {
$OSActiveSupport = ($LifeCycle.support -ge (Get-Date -Format 'yyyy-MM-dd'))
$OSSecuritySupport = ($LifeCycle.eol -ge (Get-Date -Format 'yyyy-MM-dd'))
if ($OSActiveSupport) {
Ninja-Property-Set windowsActiveSupport 1
} else {
Ninja-Property-Set windowsActiveSupport 0
}
if ($OSSecuritySupport) {
Ninja-Property-Set windowsSecuritySupport 1
} else {
Ninja-Property-Set windowsSecuritySupport 0
}
} else {
Write-Error "Support information for $ProductName $OSVersion not found from https://endoflife.date are you running an insider build?"
}

The Results

Windows Support Status Related Custom Fields

We run this on a schedule so it runs just after our OS patching applies.

Limitations

Won't work with Insider/Preview versions - hasn't been tested on Windows 7 / 8 or 8.1. Has been tested on Server 2012 R2, 2022 and Windows 10 and 11.

SpeedTest (LibreSpeed)

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding three role custom fields for devices with the Windows Desktops and Laptops and the Windows Server role:

Field NameField TypeDescription
Server UsedTextWhich server was used for the last speedtest run.
Download SpeedDecimalThe download speed from the last speedtest run in megabits per second.
Upload SpeedDecimalThe upload speed from the last speedtest run in megabits per second.

The Script

"Issues found"

This script seems to have some issues where the librespeed-cli will return null for tests intermittently - this was due to service issues with LibreSpeed themselves. An alternative version using the Ookla speedtest CLI is available below.

This Script Has Been Updated

This script was updated after being published, if you're using it please compare the version you have with the version available here.

[CmdletBinding()]
param (
# Path to download the LibreSpeed CLI executable to.
[Parameter()]
[String]$LibreSpeedEXEPath = 'C:\RMM\Bin\',
# Do not update the LibreSpeed CLI executable.
[Parameter()]
[String]$NoUpdate,
# Replace the LibreSpeed CLI executable with a fresh downloaded copy.
[Parameter()]
[String]$ForceUpdate,
# Switches for the LibreSpeed CLI executable. See documentation at https://github.com/librespeed/speedtest-cli#usage. Space separated single string.
[Parameter()]
[String]$CLISwitches
)

$LibreSpeedEXEFile = Join-Path -Path $LibreSpeedEXEPath -ChildPath 'librespeed-cli.exe'
$LibreSpeedVersionFile = Join-Path -Path $LibreSpeedEXEPath -ChildPath 'librespeed-cli.version'

if (-not (Test-Path $LibreSpeedEXEPath)) {
New-Item -ItemType Directory -Path $LibreSpeedEXEPath -Force
}
# Workaround because the CLI doesn't currently version itself properly.
if (Test-Path $LibreSpeedVersionFile) {
[version]$LibreSpeedInstalledVersion = Get-Content $LibreSpeedVersionFile -Raw
} else {
[version]$LibreSpeedInstalledVersion = [version]'0.0.0'
}

if (Test-Path $LibreSpeedEXEFile) {
$LibreSpeedCLIExists = $true
} else {
$LibreSpeedCLIExists = $false
}
if (-not $NoUpdate) {
Write-Verbose 'Starting LibreSpeed installation loop.'
$LibreSpeedReleasesURI = [uri]'https://api.github.com/repos/librespeed/speedtest-cli/releases/latest'
$Release = (Invoke-WebRequest -Uri $LibreSpeedReleasesURI -UseBasicParsing).Content | ConvertFrom-Json
[version]$ReleaseVersion = $Release.name.TrimStart('v')
if ((($LibreSpeedCLIExists) -and ($ReleaseVersion -gt $LibreSpeedInstalledVersion)) -or $ForceUpdate -or (-not $LibreSpeedCLIExists)) {
$ReleaseVersion.ToString() | Out-File -FilePath $LibreSpeedVersionFile
$Assets = $Release.assets
switch ([Environment]::Is64BitOperatingSystem) {
$true {
foreach ($Asset in $Assets | Where-Object { $_.name -like '*windows_amd64.zip' }) {
$AssetURI = $Asset.browser_download_url
$ZipFileName = $Asset.name
$ZipFilePath = "C:\RMM\$($Asset.name)"
}
}
$false {
foreach ($Asset in $Assets | Where-Object { $_.name -like '*windows_386.zip' }) {
$AssetURI = $Asset.browser_download_url
$ZipFileName = $Asset.name
$ZipFilePath = "C:\RMM\$($Asset.name)"
}
}
}
Invoke-WebRequest -Uri $AssetURI -OutFile $ZipFilePath -UseBasicParsing
if (Test-Path -Path $ZipFilePath) {
Write-Information "Extracting $ZipFileName..."
Expand-Archive -Path $ZipFilePath -DestinationPath $LibreSpeedEXEPath -Force
} else {
Write-Error 'Failed to download latest LibreSpeed CLI.'
}
} else {
Write-Information 'LibreSpeed CLI executable exists and is up to date.'
}
} else {
if (-not $LibreSpeedCLIExists) {
Write-Error 'LibreSpeed CLI executable does not exist and was not installed because -NoUpdate was specified.'
} else {
Write-Information 'LibreSpeed CLI executable exists not updating because -NoUpdate was specified.'
}
}
# Make sure the CLI switch string includes `--json` if it is not already present.
if (-not [String]::IsNullOrWhiteSpace($CLISwitches)) {
$CLISwitchArray = $CLISwitches.Split(' ')
} else {
$CLISwitchArray = @()
}
$CLISwitchArrayList = [System.Collections.ArrayList]::new()
$CLISwitchArrayList.AddRange($CLISwitchArray)
if ($CLISwitchArrayList -notcontains '--json') {
$CLISwitchArrayList.Add('--json')
}
$SpeedTestResultJSON = & $LibreSpeedEXEFile $CLISwitchArrayList
if ([String]::IsNullOrWhiteSpace($SpeedTestResultJSON)) {
Throw 'LibreSpeed CLI returned no data. This is likely due to a network issue or a problem with LibreSpeed''s servers.'
} else {
$SpeedTestResult = ConvertFrom-Json $SpeedTestResultJSON
$ServerUsed = $SpeedTestResult.server.name
$DownloadSpeed = $SpeedTestResult.download
$UploadSpeed = $SpeedTestResult.upload
Ninja-Property-Set serverUsed $ServerUsed
Ninja-Property-Set downloadSpeed $DownloadSpeed
Ninja-Property-Set uploadSpeed $UploadSpeed
}

Parameters

You can pass a few different parameters to this script to tune it's behaviour. These are:

Parameter NameTypeDescription
LibreSpeedEXEPathStringThe path to the LibreSpeed executable. Defaults to C:\RMM\Bin\
NoUpdateSwitchIf specified, the script will not attempt to update or download LibreSpeed.
ForceUpdateSwitchIf specified, the script will download LibreSpeed even if it's already up to date.
CLISwitchesStringA string of switches to pass to the LibreSpeed CLI. Defaults to --json.

For documentation on the CLI switches available see the LibreSpeed CLI documentation.

The Results

Speed Test Related Custom Fields

In this screenshot we've added a separator and a nice custom title with an emoji to make it look a bit nicer.

⏱️SPEEDTEST

We run this on demand when we want to refresh information on the device - we don't run it on a schedule at present.

Limitations

Requires the ability to run the LibreSpeed CLI tool on the device. The script can fetch the latest version and will do so automatically.

SpeedTest (Ookla Speedtest)

Creating Fields

Creating custom fields in NinjaOne

To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.

To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.

We're adding three role custom fields for devices with the Windows Desktops and Laptops and the Windows Server role:

Field NameField TypeDescription
Server UsedTextWhich server was used for the last speedtest run.
Download SpeedDecimalThe download speed from the last speedtest run in megabits per second.
Upload SpeedDecimalThe upload speed from the last speedtest run in megabits per second.

The Script

[CmdletBinding()]
param (
# URI to download the Ookla Speedtest CLI executable from.
[Parameter()]
[String]$OoklaSpeedtestURI = 'https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-win64.zip',
# Path to download the Ookla Speedtest CLI executable to.
[Parameter()]
[String]$OoklaSpeedtestEXEPath = 'C:\RMM\Bin\',
# Do not redownload the Ookla Speedtest CLI executable.
[Parameter()]
[String]$NoUpdate,
# Replace the Ookla Speedtest CLI executable with a fresh downloaded copy.
[Parameter()]
[String]$ForceUpdate,
# Switches for the Ookla Speedtest CLI executable. Space separated single string.
[Parameter()]
[String]$CLISwitches
)

$OoklaSpeedtestZipName = Split-Path -Path $OoklaSpeedtestURI -Leaf
$OoklaSpeedtestZipPath = Join-Path -Path 'C:\RMM\' -ChildPath $OoklaSpeedtestZipName
$OoklaSpeedtestEXEFile = Join-Path -Path $OoklaSpeedtestEXEPath -ChildPath 'speedtest.exe'
$OoklaSpeedtestVersionFile = Join-Path -Path $OoklaSpeedtestEXEPath -ChildPath 'ooklaspeedtest-cli.version'

if (-not (Test-Path $OoklaSpeedtestEXEPath)) {
New-Item -ItemType Directory -Path $OoklaSpeedtestEXEPath -Force
}

$OoklaspeedtestDownloadVersionMatches = Select-String -Pattern '(?<=-)(?<version>\d+.+?)(?=-)' -InputObject $OoklaSpeedtestURI
[version]$OoklaSpeedtestDownloadVersion = $OoklaspeedtestDownloadVersionMatches.Matches.Groups | Where-Object { $_.name -eq 'version' } | Select-Object -ExpandProperty value

# Workaround because the CLI doesn't currently version itself properly.
if (Test-Path $OoklaSpeedtestVersionFile) {
[version]$OoklaSpeedtestInstalledVersion = Get-Content $OoklaSpeedtestVersionFile -Raw
} else {
[version]$OoklaSpeedtestInstalledVersion = [version]'0.0.0'
$OoklaSpeedtestDownloadVersion.ToString() | Out-File -FilePath $OoklaSpeedtestVersionFile
}

if (Test-Path $OoklaSpeedtestEXEFile) {
$OoklaSpeedtestCLIExists = $true
} else {
$OoklaSpeedtestCLIExists = $false
}
if (-not $NoUpdate) {
Write-Verbose 'Starting Ookla Speedtest installation loop.'
if ((($OoklaSpeedtestCLIExists) -and ($OoklaSpeedtestDownloadVersion -gt $OoklaSpeedtestInstalledVersion)) -or $ForceUpdate -or (-not $OoklaSpeedtestCLIExists)) {
Invoke-WebRequest -Uri $OoklaSpeedtestURI -OutFile $OoklaSpeedtestZipPath -UseBasicParsing
if (Test-Path -Path $OoklaSpeedtestZipPath) {
Write-Information "Extracting $OoklaSpeedtestZipName..."
Expand-Archive -Path $OoklaSpeedtestZipPath -DestinationPath $OoklaSpeedtestEXEPath -Force
} else {
Write-Error 'Failed to download latest Ookla Speedtest CLI.'
}
} else {
Write-Information 'Ookla Speedtest CLI executable exists and is up to date.'
}
} else {
if (-not $OoklaSpeedtestCLIExists) {
Write-Error 'Ookla Speedtest CLI executable does not exist and was not installed because -NoUpdate was specified.'
} else {
Write-Information 'Ookla Speedtest CLI executable exists not updating because -NoUpdate was specified.'
}
}
# Make sure the CLI switch string includes ` --format=json-pretty`, `--accept-license` and `--accept-gdpr` if it is not already present.
if (-not [String]::IsNullOrWhiteSpace($CLISwitches)) {
$CLISwitchArray = $CLISwitches.Split(' ')
} else {
$CLISwitchArray = @()
}
$CLISwitchArrayList = [System.Collections.ArrayList]::new()
$CLISwitchArrayList.AddRange($CLISwitchArray)
if ($CLISwitchArrayList -notcontains '--format=json-pretty') {
$CLISwitchArrayList.Add('--format=json')
}
if ($CLISwitchArrayList -notcontains '--accept-license') {
$CLISwitchArrayList.Add('--accept-license')
}
if ($CLISwitchArrayList -notcontains '--accept-gdpr') {
$CLISwitchArrayList.Add('--accept-gdpr')
}
$SpeedTestResultJSON = & $OoklaSpeedtestEXEFile $CLISwitchArrayList
$SpeedTestResult = ConvertFrom-Json $SpeedTestResultJSON
$ServerUsed = '{0} ({1} - {2})' -f $SpeedTestResult.server.name, $SpeedTestResult.server.location, $SpeedTestResult.server.country
[double]$DownloadSpeed = [math]::round($SpeedTestResult.download.bandwidth / 125000, 2)
[double]$UploadSpeed = [math]::round($SpeedTestResult.upload.bandwidth / 125000, 2)
Ninja-Property-Set serverUsed $ServerUsed
Ninja-Property-Set downloadSpeed $DownloadSpeed
Ninja-Property-Set uploadSpeed $UploadSpeed

Parameters

You can pass a few different parameters to this script to tune it's behaviour. These are:

Parameter NameTypeDescription
OoklaSpeedtestURIStringThe URI to the Ookla Speedtest CLI. Defaults to the 1.2.0 version current when written.
OoklaSpeedtestEXEPathStringThe path to the Ookla Speedtest executable. Defaults to C:\RMM\Bin\
NoUpdateSwitchIf specified, the script will not attempt to update or download Ookla Speedtest.
ForceUpdateSwitchIf specified, the script will download Ookla Speedtest even if it's already up to date.
CLISwitchesStringA string of switches to pass to the Ookla Speedtest CLI. Defaults to --format=json --accept-license --accept-gdpr.

For documentation on the CLI switches available you should download the latest version of the CLI and check out the speedtest.md file in the Zip or run speedtest.exe --help.

The Results

Speed Test Related Custom Fields

In this screenshot we've added a separator and a nice custom title with an emoji to make it look a bit nicer.

⏱️SPEEDTEST

We run this on demand when we want to refresh information on the device - we don't run it on a schedule at present.

Limitations

Requires the ability to run the Ookla Speedtest CLI tool on the device. The script cannot automatically determine the latest version at this time - please pass an updated URL if you wish to use a later version before the script is updated.