Category Archives: Series: PowerShell Monitoring

Monitoring with PowerShell: Monitoring client VPN settings

So with all that’s going on a lot of people are having trouble keeping up with setting up VPNs correctly. I’ve also struggled with clients that do not have a cloud only solution but are still on a hybrid method of working.

In the past I’ve talked about Always On VPN which we tend to deploy at clients. This, and even just SSTP connections are our most used VPN method. I tend to like Microsoft solutions for everything. 😉 In any case – We’ve been having trouble with this too. Some people suggest using CMAK to assist in deploying VPN. Of course like using my RMM system instead. 😉

As with most of the blogs I’ve created two scripts; one for monitoring and one for remediation.

The monitoring script

In our RMM we can give each monitoring script a set of input variables. Using these input variables we check if the VPN is set the way we want it. If you can’t setup input variables on your RMM, just change them in the script.

$Settings = @{
    name                  = "Client based VPN"
    alluserconnection     = $true
    ServerAddress         = "remote.clientname.com"
    TunnelType            = "SSTP" #Can be: Automatic, Ikev2m L2TP, PPTP,SSTP.
    SplitTunneling        = $True 
    UseWinLogonCredential = $true
    #There's a lot more options to set/monitor. Investigate for your own settings.
}
$VPN = Get-VPNconnection -name $($Settings.name) -AllUserConnection -ErrorAction SilentlyContinue
if (!$VPN) {
    $VPNHealth = "Unhealthy - Could not find VPN Connection."    
} 
else {
    $ExpectedVPNSettings = New-Object PSCustomObject -property $Settings
    $Selection = $propsToCompare = $ExpectedVPNSettings.psobject.properties.name
    $CurrentVPNSettings = $VPN | Select-object $Selection
    $CompareVPNSettings = compare-object $CurrentVPNSettings  $ExpectedVPNSettings -Property $Selection
    if (!$CompareVPNSettings) { $VPNHealth = "Healthy" } else { $VPNHealth = "Unhealthy - Settings do not match." }
}

So now that you are monitoring the VPN connection and if the settings are correct, we’re moving on to the remediation or setup side of the house.

Remediation script

the remediation works by looking up the current VPN connections based on the name property, if the VPN does not yet exists we will add one. If it does exists, we will reset the settings to the way we would like them to be.

$Settings = @{
    name                  = "Client based VPN"
    alluserconnection     = $true
    ServerAddress         = "remote.clientname.com"
    TunnelType            = "SSTP" #Can be: Automatic, Ikev2m L2TP, PPTP,SSTP.
    SplitTunneling        = $True 
    UseWinLogonCredential = $true
    #There's a lot more options to set/monitor. Investigate for your own settings.
}
$VPN = Get-VPNconnection -name $($Settings.name) -AllUserConnection -ErrorAction SilentlyContinue
if (!$VPN) {
    Add-VPNconnection @Settings -verbose
}
else {
    Set-VpnConnection @settings -Verbose
}

What’s cool is that these scripts work for any VPN that uses the Windows VPN client. This makes it super simple to deploy and monitor your clients VPN connections, and always have the same settings across your entire customer base.

And that’s it! as always, Happy PowerShelling.

Monitoring with PowerShell: monitor and enabling WOL for HP, Lenovo, Dell

Some of my friends (Hi Joe! Hi Tyler!) recently requested a script to both monitor and enable WOL for workstations at their clients. WOL stands for Wake-On-Lan and is used to boot machines without user intervention. There are two forms of WOL: OS based, to let a machine start up from standby or hibernation, and BIOS based to boot computers which are completely turned off.

With the current stress of everyone wanting to work from home via all sorts of tools, my friends found some devices that aren’t replying to WOL packets or machines that simply were not configured for WOL yet.

I figured this could be a pretty cool exercise, I’ve seen a bunch of people making very device specific scripts to enable WOL, but not a lot of manufacture specific scripts that would enable it for an entire line of devices. During my discoveries I’ve found that the three biggest manufactures all have a method to enable WOL directly with PowerShell. Two of them use a module, the other uses a WMI class.

The script will work for Dell, HP, and Lenovo. Disclaimer: I tested the script on a handful of devices, so no guarantees of course. 🙂

The monitoring script

The monitoring script only works with PowerShell 5.0 or higher. It will also update PowerShell Get and also download the correct module for the device. We match based on the device manufacture in WMI.

$PPNuGet = Get-PackageProvider Nuget
if (!$PPNuget) {
    Write-Host "Installing Nuget provider" -foregroundcolor Green
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
}

$PSGallery = Get-PSRepository -Name PsGallery
if (!$PSGallery) {
    Write-Host "Installing PSGallery" -foregroundcolor Green
    Set-PSRepository -InstallationPolicy Trusted -Name PSGallery
}

$PsGetVersion = (get-module PowerShellGet).version
if ($PsGetVersion -lt [version]'2.0') {
    Write-Host "Installing latest version of PowerShellGet provider" -foregroundcolor Green
    install-module PowerShellGet -MinimumVersion 2.2 -Force
    Write-Host "Reloading Modules" -foregroundcolor Green
    Remove-Module PowerShellGet -Force
    Remove-module PackageManagement -Force
    Import-Module PowerShellGet -MinimumVersion 2.2 -Force
    Write-Host "Updating PowerShellGet" -foregroundcolor Green
    Install-Module -Name PowerShellGet -MinimumVersion 2.2.3 -force
    write-host "You must rerun the script to succesfully get the WOL status. PowerShellGet was found out of date." -ForegroundColor red
}

Write-Host "Checking Manufacturer" -foregroundcolor Green
$Manufacturer = (Get-WmiObject -Class:Win32_ComputerSystem).Manufacturer

if ($Manufacturer -like "*Dell*") {
    Write-Host "Manufacturer is Dell. Installing Module and trying to get WOL state" -foregroundcolor Green
    Write-Host "Installing Dell Bios Provider if needed" -foregroundcolor Green
    $Mod = Get-Module DellBIOSProvider
    if (!$mod) {
        Install-Module -Name DellBIOSProvider -Force 
    }
    import-module DellBIOSProvider

    try { 
        $WOLMonitor = get-item -Path "DellSmBios:\PowerManagement\WakeOnLan" -ErrorAction SilentlyContinue
        if($WOLMonitor.currentvalue -eq "LanOnly"){ $WOLState = "Healthy"}
    }
    catch {
        write-host "an error occured. Could not get WOL setting." 
    }
}

if ($Manufacturer -like "*HP*" -or $Manufacturer -like "*Hewlett*") {
    Write-Host "Manufacturer is HP. Installing module and trying to get WOL State." -foregroundcolor Green
    Write-Host "Installing HP Provider if needed." -foregroundcolor Green
    $Mod = Get-Module HPCMSL
    if (!$mod) {
        Install-Module -Name HPCMSL -Force -AcceptLicense
    }
    
    import-module HPCMSL
    try { 
        $WolTypes = get-hpbiossettingslist | Where-Object { $_.Name -like "*Wake On Lan*" }
        $WOLState = ForEach ($WolType in $WolTypes) {
            write-host "Setting WOL Type: $($WOLType.Name)"
            get-HPBIOSSettingValue -name $($WolType.name) -ErrorAction Stop 
        }
    }
    catch {
        write-host "an error occured. Could not find WOL state" 
    }
}

if ($Manufacturer -like "*Lenovo*") {
    Write-Host "Manufacturer is Lenovo. Trying to get via WMI" -foregroundcolor Green

    try { 
        Write-Host "Getting BIOS." -foregroundcolor Green
        $currentSetting = (Get-WmiObject -ErrorAction Stop -class "Lenovo_BiosSetting" -namespace "root\wmi") | Where-Object { $_.CurrentSetting -ne "" }
        $WOLStatus = $currentSetting.currentsetting | ConvertFrom-Csv -Delimiter "," -Header "Setting", "Status" | Where-Object { $_.setting -eq "Wake on lan" }
        $WOLStatus = $WOLStatus.status -split ";"
        if ($WOLStatus[0] -eq "Primary") { $WOLState = "Healthy" }
    }
    catch {
        write-host "an error occured. Could not find WOL state" 
    }
}
$NicsWithWake = Get-CimInstance -ClassName "MSPower_DeviceWakeEnable" -Namespace "root/wmi" | Where-Object { $_.Enable -eq $false }

if (!$NicsWithWake) {
    $NICWOL = "Healthy - All NICs enabled for WOL within the OS." 
} 
else {
    $NICWOL = "Unhealthy. NIC does not have WOL enabled inside of the OS." 
}

if (!$WOLState) { 
    $WOLState = "Unhealthy - Could not find WOL state" 
}

After running the script we can check the contents of $WOLState for the current state of the WOL setting in the BIOS. You can also check $NICWOL for the Operating System’s WOL state.

The remediation script

So on the remediation side we tackle enabling WOL in both the BIOS and inside of the Operating System. We always install the module in this case, as often module updates are released when newer systems are too.

$PPNuGet = Get-PackageProvider Nuget
if (!$PPNuget) {
    Write-Host "Installing Nuget provider" -foregroundcolor Green
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
}

$PSGallery = Get-PSRepository -Name PsGallery
if (!$PSGallery) {
    Write-Host "Installing PSGallery" -foregroundcolor Green
    Set-PSRepository -InstallationPolicy Trusted -Name PSGallery
}

$PsGetVersion = (get-module PowerShellGet).version
if ($PsGetVersion -lt [version]'2.0') {
    Write-Host "Installing latest version of PowerShellGet provider" -foregroundcolor Green
    install-module PowerShellGet -MinimumVersion 2.2 -Force
    Write-Host "Reloading Modules" -foregroundcolor Green
    Remove-Module PowerShellGet -Force
    Remove-module PackageManagement -Force
    Import-Module PowerShellGet -MinimumVersion 2.2 -Force
    Write-Host "Updating PowerShellGet" -foregroundcolor Green
    Install-Module -Name PowerShellGet -MinimumVersion 2.2.3 -force
    write-host "You must rerun the script to succesfully set the WOL status. PowerShellGet was found out of date." -ForegroundColor red
}
Write-Host "Checking Manufacturer" -foregroundcolor Green
$Manufacturer = (Get-WmiObject -Class:Win32_ComputerSystem).Manufacturer

if ($Manufacturer -like "*Dell*") {
    Write-Host "Manufacturer is Dell. Installing Module and trying to enable Wake on LAN." -foregroundcolor Green
    Write-Host "Installing Dell Bios Provider" -foregroundcolor Green
    Install-Module -Name DellBIOSProvider -Force 
    import-module DellBIOSProvider
    try { 
        set-item -Path "DellSmBios:\PowerManagement\WakeOnLan" -value "LANOnly" -ErrorAction Stop
    }
    catch {
        write-host "an error occured. Could not set BIOS to WakeOnLan. Please try setting WOL manually" 
    }
}

if ($Manufacturer -like "*HP*" -or $Manufacturer -like "*Hewlett*") {
    Write-Host "Manufacturer is HP. Installing module and trying to enable WakeOnLan. All HP Drivers are required for this operation to succeed." -foregroundcolor Green
    Write-Host "Installing HP Provider" -foregroundcolor Green
    Install-Module -Name HPCMSL -Force -AcceptLicense
    import-module HPCMSL
    try { 
        $WolTypes = get-hpbiossettingslist | Where-Object { $_.Name -like "*Wake On Lan*" }
        ForEach ($WolType in $WolTypes) {
            write-host "Setting WOL Type: $($WOLType.Name)"
            Set-HPBIOSSettingValue -name $($WolType.name) -Value "Boot to Hard Drive" -ErrorAction Stop 
        }
    }
    catch {
        write-host "an error occured. Could not set BIOS to WakeOnLan. Please try manually" 
    }
}

if ($Manufacturer -like "*Lenovo*") {
    Write-Host "Manufacturer is Lenovo. Trying to set via WMI. All Lenovo Drivers are required for this operation to succeed." -foregroundcolor Green

    try { 
        Write-Host "Setting BIOS." -foregroundcolor Green
        (Get-WmiObject -ErrorAction Stop -class "Lenovo_SetBiosSetting" -namespace "root\wmi").SetBiosSetting('WakeOnLAN,Primary') | Out-Null
        Write-Host "Saving BIOS." -foregroundcolor Green
        (Get-WmiObject -ErrorAction Stop -class "Lenovo_SaveBiosSettings" -namespace "root\wmi").SaveBiosSettings() | Out-Null

    }
    catch {
        write-host "an error occured. Could not set BIOS to WakeOnLan. Please try manually" 
    }
}


write-host "Setting NIC to enable WOL" -ForegroundColor Green

$NicsWithWake = Get-CimInstance -ClassName "MSPower_DeviceWakeEnable" -Namespace "root/wmi"

foreach ($Nic in $NicsWithWake) {
    write-host "Enabling for NIC" -ForegroundColor green
    Set-CimInstance $NIC -Property @{Enable = $true }
}

Like I said in the start – This still is slightly experimental for me. I did not have a large stack of devices to test on other than Dell devices, so if you find any issues with Lenovo or HP devices, let me know and send me a transcript, maybe I can help you figure it out!

And that’s it. As always, Happy PowerShelling.

update: N-Central remediation AMP can be found here. The monitoring AMP can be found here.

Update 2: Fixed a small encoding issue that crashed the script in some cases.

Monitoring with PowerShell: VSS Snapshot size

We’ve been doing some work for another IT company of a friend of mine. I’ve been helping him with automation inside of his RMM system. He is like me and really likes to have VSS as a third or fourth backup solution, just for those small emergencies when a file is deleted.

The issue with this is that VSS snapshots can sometimes consume a lot of space. The default a VSS snapshot can consume is 10 percent of the entire disk. On larger disks such as file servers this can be hundreds of gigabytes. Instead of just lowering the quota I’ve decided to create a small monitor that can alert when a specific threshold has been reached and we can react on that. This way we can just let the VSS snapshot size be the 10% for all our clients, and only change the ones where we see it will consume a lot of disk space.

The script uses the cim/wmi instance “Win32_ShadowStorage to retrieve the correct sizes and compares it to the threshold.

$threshold = "600" #threshold in GB
$DiskSpaceUsed = Get-CimInstance -ClassName Win32_ShadowStorage | Select-Object @{n = "Used (GB)"; e = { [math]::Round([double]$_.UsedSpace / 1GB, 3) } }, @{n = "Max (GB)"; e = { [math]::Round([double]$_.MAxSpace / 1GB, 3) } }, *
$HealthState = foreach ($Disks in $DiskSpaceUsed) {

    $Volume = get-volume -UniqueId $DiskSpaceUsed.Volume.DeviceID
    $DiskSize = [math]::Round([double]$volume.Size / 1GB, 3)
    $diskremaining = [math]::Round([double]$volume.SizeRemaining / 1GB, 3)
    if ($Disks.'Used (GB)' -gt $threshold) { "Disk $($Volume.DriveLetter) snapshot size is higher than $Threshold. The disk size is $($diskSize) and it has $($diskremaining) remaining space. The max snapshot size is $($Disks.'Max (GB)')" }
}

if (!$HealthState) {
    $HealthState = "Healthy"
}

And that’s it! a short but sweet one. As always, Happy PowerShelling!

Monitoring with PowerShell: Monitoring Onedrive and Sharepoint file limits

I love our cloud deployments. I’m amazed at how well people are adapting to working with an online online environment and using the tools cross platform such as the Onedrive and Teams client. As both a tech, and on the management side of the house it’s great to see such flexibility.

The only downside we’ve found so far is that you do have pay a lot of attention to the current limitations of the application your running. So in our case we have a bunch of OneDrive clients. These clients have to be taught you can’t just drop everything in a single location like we did with a file server.

So we are now monitoring some of the limitations that exist in the OneDrive sync client. One of the limitations is that you should not sync libraries with more than 100,000 files, so whenever we reach 90000 files, we create a new library or move data for our clients.

All Tenants Script

The script uses the Graph API. Get all the keys from the Secure App Model script. It will run for all tenants and alert on each that has more than 90000 files.

$ApplicationId = 'YOURAPPLICATIONID'
$ApplicationSecret = 'YOURAPPLICATIONSECRET' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YOURTENANTID'
$RefreshToken = 'YOURUNBELIEVEBALLYLONGREFRESHTOKEN'
$upn = 'UPN-Used-To-Generate-Tokens'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
write-host "Generating access tokens" -ForegroundColor Green
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID
write-host "Connecting to MSOLService" -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
write-host "Grabbing client list" -ForegroundColor Green
$customers = Get-MsolPartnerContract -All
write-host "Connecting to clients" -ForegroundColor Green

$LimitsReached = foreach ($customer in $customers) {
    write-host "Generating token for $($Customer.name)" -ForegroundColor Green
    $graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $customer.TenantID
    $Header = @{
        Authorization = "Bearer $($graphToken.AccessToken)"
    }
    Write-Host   "Grabbing data for $($customer.name)" -ForegroundColor green
    $OneDriveUsageURI = "https://graph.microsoft.com/v1.0/reports/getOneDriveUsageAccountDetail(period='D7')"
    $OneDriveUsageReports = (Invoke-RestMethod -Uri $OneDriveUsageURI -Headers $Header -Method Get -ContentType "application/json") | ConvertFrom-Csv

    $SharepointUsageReportsURI = "https://graph.microsoft.com/v1.0/reports/getSharePointSiteUsageDetail(period='D7')"
    $SharepointUsageReports = (Invoke-RestMethod -Uri $SharepointUsageReportsURI -Headers $Header -Method Get -ContentType "application/json") | ConvertFrom-Csv
    
    
    foreach ($SharepointReport in $SharepointUsageReports) {
        if ([int]$SharepointReport.'File count' -ge [int]"90000") {
            $SharepointReport 
        }
    }

    foreach ($OneDriveReport in $OneDriveUsageReports) {
        if ([int]$OneDriveReport.'File count' -ge [int]"90000") {
        $OneDriveReport
        }
    }

     

}

if (!$LimitsReached) {
    Write-Host   "Healthy" -ForegroundColor green
}
else {
    Write-Host   "Unhealthy" -ForegroundColor Red
    $LimitsReached
}

Single Tenant Script

This version runs only for the tenant you’ve entered, so you can specify which tenants you want to monitor.

$ApplicationId = 'YOURAPPLICATIONID'
$ApplicationSecret = 'YOURAPPLICATIONSECRET' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YOURTENANTID'
$RefreshToken = 'YOURUNBELIEVEBALLYLONGREFRESHTOKEN'
$upn = 'UPN-Used-To-Generate-Tokens'
$TenantToMonitor = "Blabla.onmicrosoft.com"
##############################

$LimitsReached = 

    write-host "Generating token for $($TenantToMonitor)" -ForegroundColor Green
    $graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $TenantToMonitor
    $Header = @{
        Authorization = "Bearer $($graphToken.AccessToken)"
    }
    Write-Host   "Grabbing data for $($TenantToMonitor)" -ForegroundColor green
    $OneDriveUsageURI = "https://graph.microsoft.com/v1.0/reports/getOneDriveUsageAccountDetail(period='D7')"
    $OneDriveUsageReports = (Invoke-RestMethod -Uri $OneDriveUsageURI -Headers $Header -Method Get -ContentType "application/json") | ConvertFrom-Csv

    $SharepointUsageReportsURI = "https://graph.microsoft.com/v1.0/reports/getSharePointSiteUsageDetail(period='D7')"
    $SharepointUsageReports = (Invoke-RestMethod -Uri $SharepointUsageReportsURI -Headers $Header -Method Get -ContentType "application/json") | ConvertFrom-Csv
    
    
    foreach ($SharepointReport in $SharepointUsageReports) {
        if ([int]$SharepointReport.'File count' -ge [int]"90000") {
            $SharepointReport 
        }
    }

    foreach ($OneDriveReport in $OneDriveUsageReports) {
        if ([int]$OneDriveReport.'File count' -ge [int]"90000") {
        $OneDriveReport
        }
    }

     


if (!$LimitsReached) {
    Write-Host   "Healthy" -ForegroundColor green
}
else {
    Write-Host   "Unhealthy" -ForegroundColor Red
    $LimitsReached
}

So that’s it. As always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring Active SMB sessions.

So with the new SMBv3 Remote Code Execution issues codenamed “SMBGhost”. SMBGhost is an issue where an attack could gain remote code execution by exploiting a bug in SMB compression. A temporary fix is disabling SMB compression on the server side using this registry key:

Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" DisableCompression -Type DWORD -Value 1 -Force

Microsoft has since released a patch (see this link for more info). We’ve decided to start monitoring SMB sessions on clients in any case. Normally speaking, no SMB sessions to a client should be open unless you are performing a remote installation using the ADMIN$ share. So it’s good practice to check if there are SMB sessions open and if so, where they are coming from. This is also a pretty cool trick to find who is hosting their own shares inside of your networks.

The Script

So its a fairly short script – it alerts on both currently opened sessions, and active SMB connections. There’s a difference between the both as you can connect to the IPC$ share, without having an active open session. In any case – I’d run this script every minute or less on all your workstations. Its quite lightweight and a great help to find bad actors in your environment.

$Sessions = Get-smbsession
$Connections = get-smbconnection


if ($sessions) {
    foreach ($Session in $Sessions) {
        write-host "a session has been found coming from $($Session.ClientComputerName). The logged on user is $($Session.ClientUserName) with $($Session.NumOpens) opened sessions" 
    }
}
else {
    write-host "No sessions found"
}

if ($Connections) {
    foreach ($Connection in $Connections) {
        write-host "a Connection has been found on $($Connection.ServerName). The logged on user is $($Connection.Username) with $($Connection.NumOpens) opened sessions" 
    }
}
else {
    write-host "No sessions found"
}

And that’s it! as always, Happy PowerShelling. 🙂

Monitoring with PowerShell: Monitoring Unifi site configuration

So I’ve done a couple of blogs about Unifi before. You can find those here, here, and here. I really like the entire Ubiquiti Unifi stack thanks to the ease of configuration. This ease of configuration does make it so that everyone can install it, even though mistakes can be made.

These mistakes or small configuration errors are the reason I’ve made a monitoring set to check if each site is configured the way we prefer it at my company.

So lets get started; first we connect to the API using the following script:

param(
    [string]$URL = 'yourcontroller.controller.tld',
    [string]$port = '8443',
    [string]$User = 'APIUSER',
    [string]$Pass = 'SomeReallyLongPassword',
    [string]$SiteCode = 'default' #you can enter each site here. This way when you assign the monitoring to a client you edit this to match the correct siteID.
)
[string]$controller = "https://$($URL):$($port)"
[string]$credential = "`{`"username`":`"$User`",`"password`":`"$Pass`"`}"
try {
    $null = Invoke-Restmethod -Uri "$controller/api/login" -method post -body $credential -ContentType "application/json; charset=utf-8"  -SessionVariable myWebSession
}
catch {
    $APIerror = "Api Connection Error: $($_.Exception.Message)"
}

Now that we’re connected, we can start making queries. Check out the older unifi blogs if you just want to focus on device monitoring. in this case we’re going to be checking our configuration and if it matches the following, this is not our exact configuration but with these settings you’d be able to edit it to anything you want. 🙂

  • We want at least 3 networks to be available: LAN, Guest, VOIP.
  • We want to make sure the ALG settings are disabled.
  • Speedtest must be enabled and running every 20 minutes.
  • Also, we want “Advanced Feature Mode” to be enabled.

We’re going to be downloading 2 arrays from the Unifi API. One for the Network Configuration, the other for the Site Configuration. I’ve placed it all in an object, which most RMM systems can’t really alert on, which is why I’ve also included the if/else statements all the way at the bottom. You can change these to your own wishes easily.

param(
    [string]$URL = 'yourcontroller.controller.tld',
    [string]$port = '8443',
    [string]$User = 'APIUSER',
    [string]$Pass = 'SomeReallyLongPassword',
    [string]$SiteCode = 'default' #you can enter each site here. This way when you assign the monitoring to a client you edit this to match the correct siteID.
)
[string]$controller = "https://$($URL):$($port)"
[string]$credential = "`{`"username`":`"$User`",`"password`":`"$Pass`"`}"


$errorlist = New-Object -TypeName PSCustomObject
try {
    $null = Invoke-Restmethod -Uri "$controller/api/login" -method post -body $credential -ContentType "application/json; charset=utf-8"  -SessionVariable myWebSession
}
catch {
    Add-Member -InputObject $ErrorList -MemberType NoteProperty -Name APISessionError -Value $_.Exception.Message
}

try {
    $NetWorkConf = (Invoke-Restmethod -Uri "$controller/api/s/$SiteCode/list/networkconf" -WebSession $myWebSession).data | Where-Object { $_.Purpose -ne "WAN" }
}
catch {
    Add-Member -InputObject $ErrorList -MemberType NoteProperty -Name APINetworkError -Value $_.Exception.Message
}

try {
    $SysInfo = (Invoke-Restmethod -Uri "$controller/api/s/$SiteCode/get/setting" -WebSession $myWebSession).data
}
catch {
    Add-Member -InputObject $ErrorList -MemberType NoteProperty -Name APISysInfoError -Value $_.Exception.Message
}

$UnifiOutput = [PSCustomObject]@{
    NetworkNames      = $Networkconf.name
    NetworkCount      = $NetWorkConf.Count
    AdvancedFeatures  = ($Sysinfo.advanced_feature_enabled)
    SpeedTestEnabled  = ($sysinfo | Where-Object { $_.key -eq "Auto_Speedtest" }).enabled
    SpeedTestInterval = ($sysinfo | Where-Object { $_.key -eq "Auto_Speedtest" }).interval
    VoipNetwork       = ($NetWorkConf.name | Where-Object { $_ -like "*VOIP*" }).Count
    GuestNetwork      = ($NetWorkConf.purpose | Where-Object { $_ -like "*guest*" }).Count
    LANNetworks       = ($NetWorkConf.name | Where-Object { $_ -like "*-LAN*" }).Count
    Modules           = [PSCustomObject]@{
        ftp_module           =	$sysinfo.ftp_module
        gre_module           =	$sysinfo.gre_module
        h323_module          =	$sysinfo.h323_module
        pptp_module          =	$sysinfo.pptp_module
        sip_module           =	$sysinfo.sip_module
        tftp_module          =	$sysinfo.tftp_module
        broadcast_ping       =	$sysinfo.broadcast_ping
        receive_redirects    =	$sysinfo.receive_redirects
        send_redirects       =	$sysinfo.send_redirects
        syn_cookies          =	$sysinfo.syn_cookies
        offload_accounting   =	$sysinfo.offload_accounting
        offload_sch          =	$sysinfo.offload_sch
        offload_l2_blocking  =	$sysinfo.offload_l2_blocking
        mdns_enabled         =	$sysinfo.mdns_enabled
        upnp_enabled         =	$sysinfo.upnp_enabled
        upnp_nat_pmp_enabled =	$sysinfo.upnp_nat_pmp_enabled
        upnp_secure_mode     =	$sysinfo.upnp_secure_mode
        mss_clamp            =	$sysinfo.mss_clamp
    }
}

if ($UnifiOutput.NetworkCount -lt "3") { write-host "Not enough networks found. Only 3 are present." }
if ($UnifiOutput.SpeedTestEnabled -eq $false) { write-host "Speedtest disabled" }
if ($UnifiOutput.SpeedTestInterval -gt "20") { write-host "Speedtest is not set to run every 20 minutes." }
if ($UnifiOutput.SpeedTestInterval -gt "20") { write-host "Speedtest is not set to run every 20 minutes." }
if ($UnifiOutput.Modules.sip_module -eq $true) { Write-Host "SIP ALG Module is enabled." }

And that’s it. As always, Happy PowerShelling. 🙂

Using PowerShell to generate and deploy Group Policies for non-domain environments

So I’ve been having a hard time coming up with a title for this one. As I’ve stated in previous blogs we’re moving more and more clients to cloud only environments using Azure AD, Teams, and Onedrive as their collaboration and file sharing solution. The issue with this was that it became quite difficult to deploy GPOs. This was especially bad when wanting to use ADMX templates. We could use Intune but found that even there we had so many limitation it would not work for us.

Full disclosure; There are some third party applications that offer GPO deployment. We found most of them to be overkill for what we needed, or not very suitable for the MSP market. I mean – We already have our RMM system, and tacking on another application just did not seem right to me.

So to solve the issues with GPO deployment for Azure AD environments, or workgroup environments I’ve created a PowerShell script that allows you to deploy group policies. I’ve also created a script to monitor if the deployment ran correctly. That way you can use your RMM to see who received the new policy and who has not.

Execution Script

Before we get started on the script, you will need the following two items: LGPO.exe, which we’ll use to export and deploy the policy, and winrar which will create our setup file. We download the LGPO.exe for you (Please host this somewhere you trust.). Winrar you’ll need to install yourself.

Disclaimer/warning: Please note that the script is destructive to the currently installed local group policies. Please run the script inside of a VM or machine that does not have any local policies. It will clear all policies by destroying the actual policy files. You have been warned 🙂

So the steps are straightforward – Save the script and execute it with the parameters you need. The script will delete the current GPOs, and then open the group policy editor. Import your ADMX files, edit the settings, and close the editor. Then the script will finish up and put 2 files on your desktop – One to apply the policy, the other to remove the policy in case you no longer need it.

<#
.SYNOPSIS
  Creates an execuble that can apply and remove a local Group Policy Object
.DESCRIPTION
 Creates an execuble that can apply and remove a specific local policy. The executable is a self-extracting winrar archive. Winrar installation is required for processing.
 The execuble will work in "MERGE" mode, meaning that settings that are duplicate will be overwritten, other settings will not be touched.

 Parameters are not required, but optional. Script will fail if LGPO and Winrar are not present.

 WARNING: SCRIPT IS DESTRUCTIVE TO LOCAL GROUP POLICIES. DO NOT RUN ON PRODUCTION MACHINES. Use at own risk.
.PARAMETER DownloadURL
    Specificies where to download LGPO from if not installed.
.PARAMETER DownloadLocation
    Specificies where to download LGPO to if not installed. Defaults to C:\Temp\LGPO
.PARAMETER WorkingPath
    Specificies where to put temporary files. Defaults to C:\Temp\LGPO
.PARAMETER WinrarPath
    Path where winrar is found. Defaults to C:\Program Files\WinRAR\WinRAR.exe
.PARAMETER GPOName
    Decides part of the name of the file that will be placed in C:\ProgramData\
.PARAMETER GPOVersion
    Decides part of the name of the file that will be placed in C:\ProgramData\
.INPUTS
  none
.OUTPUTS
  executable generated and stored in user desktop location
.NOTES
  Version:        0.4
  Author:         Kelvin Tegelaar
  Creation Date:  02/2020
  Purpose/Change: Initial script. Beta.

#>
Param(
    [string]$DownloadURL = "http://cyberdrain.com/wp-content/uploads/2020/02/LGPO.exe",
    [string]$DownloadLocation = "C:\Temp\LGPO",
    [string]$WinrarPath = "C:\Program Files\WinRAR\WinRAR.exe",
    [string]$GPOName = "GPO",
    [string]$GPOVersion = "1.0"
)

write-host "Checking if base folder exists in $DownloadLocation and if not, creating it." -ForegroundColor Green
try {
    $TestDownloadLocation = Test-Path $DownloadLocation
    if (!$TestDownloadLocation) {
        write-host "Creating Folder to download LGPO." -ForegroundColor Green
        new-item $DownloadLocation -ItemType Directory -force
    }
    $TestDownloadLocationExe = Test-Path "$DownloadLocation\LGPO.exe"
    if (!$TestDownloadLocationExe) { 
        write-host "Download LGPO." -ForegroundColor Green
        Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile "$DownloadLocation\LGPO.exe" 
    }
    $TestDownloadLocationBat = Test-Path "$DownloadLocation\LGPOExecute.bat"
    if (!$TestDownloadLocationBat) { 
        write-host "Generating configuration batch file." -ForegroundColor Green

        @"
LGPO.exe /t ComputerPolicy.txt /v > "C:\ProgramData\$GPOName $GPOVersion Computer.log"
LGPO.exe /t UserPolicy.txt /v > "C:\ProgramData\$GPOName $GPOVersion User.log"
"@ | Out-File -Encoding ascii "$DownloadLocation\LGPOExecute.bat" -Force
    
    }
}
catch {
    write-host "The download and extraction of LGPO.EXE failed. Error: $($_.Exception.Message)" -ForegroundColor Red
    exit 1
}
write-host "Clearing all existing policies." -ForegroundColor Green
#Clears all local policies
remove-item -Recurse -Path "$($ENV:windir)\System32\GroupPolicyUsers" -Force -erroraction silentlycontinue
remove-item -Recurse -Path "$($ENV:windir)\System32\GroupPolicy" -Force -erroraction silentlycontinue
remove-item -Recurse -Path "$DownloadLocation\ComputerPolicy.txt" -Force -erroraction silentlycontinue
remove-item -Recurse -Path "$DownloadLocation\Userpolicy.txt" -Force -erroraction silentlycontinue
write-host "Running GPUpdate to clear local policy cache." -ForegroundColor Green
gpupdate /force
write-host "Starting GPEdit. Please create your policy. After closing GPEdit we will resume." -ForegroundColor Green
start-process "gpedit.msc" -Wait
write-host "Exporting policies with LGPO." -ForegroundColor Green
& "$DownloadLocation\LGPO.EXE" /parse /m "$($ENV:windir)\System32\GroupPolicy\Machine\Registry.pol" > $DownloadLocation\ComputerPolicy.txt
& "$DownloadLocation\LGPO.EXE"  /parse /u "$($ENV:windir)\System32\GroupPolicy\User\Registry.pol" > $DownloadLocation\Userpolicy.txt
write-host "Sleeping for 10 seconds to give LGPO a chance to export all settings if GPO is large." -ForegroundColor Green
start-sleep 10
$UserDesktop = [Environment]::GetFolderPath("Desktop")
@"
Setup=LGPOExecute.bat
TempMode
Silent=1
"@ | out-file "$DownloadLocation\SFXConfig.conf" -Force

write-host "Creating Apply executable and placing on current user desktop." -ForegroundColor Green
& $WinrarPath -s a -ep1 -r -o+ -dh -ibck -sfx  -iadm -z"C:\temp\LGPO\SFXConfig.conf" "$UserDesktop\$GPOName $GPOVersion Apply Policy.exe" "$DownloadLocation\*"
start-sleep 3

write-host "Creating Remove executable and placing on current user desktop." -ForegroundColor Green
$ComputerPolicy = get-content "$DownloadLocation\ComputerPolicy.txt"
$UserPolicy = get-content "$DownloadLocation\UserPolicy.txt"
$ReplacementArray = @("DELETEKEYS", "DELETE", "QWORD", "SZ", "EXSZ", "MULTISZ", "BINARY", "CREATEKEY", "DELETEALLVALUES", "DWORD")
foreach ($Replacement in $ReplacementArray) {
    $ComputerPolicy = $ComputerPolicy | Foreach-Object { $_ -replace "^.*$replacement.*$", "CLEAR" }
    $UserPolicy = $UserPolicy | Foreach-Object { $_ -replace "^.*$replacement.*$", "CLEAR" }
}
$UserPolicy | out-file "$DownloadLocation\Userpolicy.txt"
$ComputerPolicy | out-file "$DownloadLocation\ComputerPolicy.txt"

& $winrarPath -s a -ep1 -r -o+ -dh -ibck -sfx  -iadm -z"C:\temp\LGPO\SFXConfig.conf" "$UserDesktop\$GPOName $GPOVersion Remove Policy.exe" "$DownloadLocation\*"
remove-item -Recurse -Path "$DownloadLocation\LGPOExecute.bat" -Force -erroraction silentlycontinue

Deploy the files on your desktop as-if its an application via your RMM.

Monitoring the deployment

So you can run the executable and when it runs it will create 2 log files in C:\ProgramData, one for User policies, the other for Computer policies. To monitor these, you check if the file exists and if it contains “Policy saved” in the last 3 lines.

$GPOFile = "C:\ProgramData\GPO 1.0 User.log"

$Check = Test-Path "C:\ProgramData\GPO 1.0 User.log"
if (!$Check) { 
    $Healthstate = "GPO has not deployed. Log file does not exist."
}
else {
    $State = get-content $GPOFile | Select-Object -last 3
    if ($state[0] -ne "POLICY SAVED.") { $Healthstate = "GPO Log found but policy not saved." } else { $Healthstate = "Healthy" }
}

And that’s it! With this you’ll be able to deploy GPOs as if you still have your local AD domain, something we really missed in our cloud only deployments. As always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring Dell Driver Updates (DCU 3.1)

Previously I’ve written a blog about Dell Command Update and its ability to monitoring and download updates. This blog was based on Dell Command Update 2. As it is with all applications this started working less on newer machines. To resolve this Dell released a new major update for Dell Command Update which according to Dell, works on 99% of the Dell devices.

I really like monitoring if the device drivers are up to date, and all versions are as current as can be. Dell Command Update also allows you to install the updates on the device for remediation\

Updates Detection Script

The monitoring script downloads a zip file with the Dell Command Update utility. You can create this zip-file yourself by installing Dell Command Update and simply zipping the install location. It then unzips the downloaded file, and runs the DCU-cli with the Report Parameter, I would advise to only run this set on an hourly or even daily schedule, using your RMM system of course.

So I’d really suggest to host the file yourself, either by creating your own DCU.zip or downloading the one included in the script below. I will be removing this from my web host in the future. 🙂

You can choose what variables to alert on yourself – I like reporting on the count of updates, but I know others rather would alert on the title. At the bottom of the scripts I’ve added specific alerting options – You can choose which of these you find important.

#Replace the Download URL to where you've uploaded the ZIP file yourself. We will only download this file once. 
$DownloadURL = "https://cyberdrain.com/wp-content/uploads/2020/02/DCU.zip"
$DownloadLocation = "$($Env:ProgramData)\DCU"
try {
    $TestDownloadLocation = Test-Path $DownloadLocation
    if (!$TestDownloadLocation) { new-item $DownloadLocation -ItemType Directory -force }
    $TestDownloadLocationZip = Test-Path "$DownloadLocation\DCU.zip"
    if (!$TestDownloadLocationZip) { Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile "$($DownloadLocation)\DCU.zip" }
    $TestDownloadLocationExe = Test-Path "$DownloadLocation\dcu-cli.exe"
    if (!$TestDownloadLocationExe) { Expand-Archive "$($DownloadLocation)\DCU.zip" -DestinationPath $DownloadLocation -Force }
}
catch {
    write-host "The download and extraction of DCUCli failed. Error: $($_.Exception.Message)"
    exit 1
}

start-process "$($DownloadLocation)\dcu-cli.exe" -ArgumentList "/scan -report=$ENV:temp\DCU" -Wait
[ xml]$XMLReport = get-content "$ENV:temp\DCU\DCUApplicableUpdates.xml" 
#We now remove the item, because we don't need it anymore, and sometimes fails to overwrite
remove-item "$ENV:temp\DCU\DCUApplicableUpdates.xml" -Force

$AvailableUpdates = $XMLReport.updates.update

$BIOSUpdates        = ($XMLReport.updates.update | Where-Object {$_.type -eq "BIOS"}).name.Count
$ApplicationUpdates = ($XMLReport.updates.update | Where-Object {$_.type -eq "Application"}).name.Count
$DriverUpdates      = ($XMLReport.updates.update | Where-Object {$_.type -eq "Driver"}).name.Count
$FirmwareUpdates    = ($XMLReport.updates.update | Where-Object {$_.type -eq "Firmware"}).name.Count
$OtherUpdates       = ($XMLReport.updates.update | Where-Object {$_.type -eq "Other"}).name.Count
$PatchUpdates       = ($XMLReport.updates.update | Where-Object {$_.type -eq "Patch"}).name.Count
$UtilityUpdates     = ($XMLReport.updates.update | Where-Object {$_.type -eq "Utility"}).name.Count
$UrgentUpdates      = ($XMLReport.updates.update | Where-Object {$_.Urgency -eq "Urgent"}).name.Count

So that’s the detecting updates portion, of course we also have the commandline to install the updates. Lets get started with that.

Remediation

Remediation is fairly straight forward. When using the switch /ApplyUpdates the updates start immediately. Of course we like having a little more control, so all the options are listed here. I’ve also included some examples:

Installing all updates, disable bitlocker, and reboot if required:

$DownloadLocation = "$($Env:ProgramData)\DCU"
start-process "$($DownloadLocation)\dcu-cli.exe" -ArgumentList "/applyUpdates -autoSuspendBitLocker=enable -reboot=enable" -Wait

This installs all available update found during the last scan including BIOS updates, suspends bitlocker, and reboots the computer immediately.

Installing all updates, do not disable Bitlocker, and do not reboot

$DownloadLocation = "$($Env:ProgramData)\DCU"
start-process "$($DownloadLocation)\dcu-cli.exe" -ArgumentList "/applyUpdates -autoSuspendBitLocker=disable -reboot=disable" -Wait

This installs all available update found during the last scan excluding BIOS updates, because we aren’t suspending bitlocker, and lets the user reboot the computer.

Install BIOS updates, suspend bitlocker, reboot

$DownloadLocation = "$($Env:ProgramData)\DCU"
start-process "$($DownloadLocation)\dcu-cli.exe" -ArgumentList "/applyUpdates -autoSuspendBitLocker=enable -reboot=enable -updateType=bios" -Wait

And this one installs only the BIOS updates. I think with these examples and the manual I’ve posted above you can figure out your exact preferred settings.

So that’s it! As always, Happy PowerShelling!

Monitoring with PowerShell: Monitoring SMART status using SmartCTL.

Some time ago I wrote a blog about monitoring SMART status with CrystalDiskInfo. After bringing this script over to our production RMM environment everything seemed good. But when I looked a little deeper I found that the script failed on NVME drives. NVME drives handle SMART-Status different from ‘regular’ SATA drives.

This started me on a quest for a solution that also worked on NVME drives. I’ve decided to use SmartMonTools as it has the same benefits as crystaldiskmark – It’s portable, does not require an installation, and is small enough to be downloaded on demand.

The script is fairly straightforward, it downloads the utility from a host, extracts the utility and runs an update for SmartCTL so it can fill in the data correctly. After this for each HDD in the system it will run a compare to the thresholds you’ve setup.

I’ve also had a request for disk monitoring on specifically the available spare count. The script can be edited to monitor this too – that way you can decide your own thresholds over what the manufacturer said is default for the disk.

The script

############ Thresholds #############
$PowerOnTime = 35063 #about 4 years constant runtime.
$PowerCycles = 4000 #4000 times of turning drive on and off
$Temperature = 60 #60 degrees celcius
############ End Thresholds #########
$DownloadURL = "https://cyberdrain.com/wp-content/uploads/2020/02/Smartmontools.zip"
$DownloadLocation = "$($Env:ProgramData)\SmartmonTools"
try {
    $TestDownloadLocation = Test-Path $DownloadLocation
    if (!$TestDownloadLocation) { new-item $DownloadLocation -ItemType Directory -force }
    $TestDownloadLocationZip = Test-Path "$DownloadLocation\Smartmontools.zip"
    if (!$TestDownloadLocationZip) { Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile "$($DownloadLocation)\Smartmontools.zip" }
    $TestDownloadLocationExe = Test-Path "$DownloadLocation\smartctl.exe"
    if (!$TestDownloadLocationExe) { Expand-Archive "$($DownloadLocation)\Smartmontools.zip" -DestinationPath $DownloadLocation -Force }
}
catch {
    write-host "The download and extraction of SMARTCTL failed. Error: $($_.Exception.Message)"
    exit 1
}
#update the smartmontools database
start-process -filepath "$DownloadLocation\update-smart-drivedb.exe" -ArgumentList "/S" -Wait
#find all connected HDDs
$HDDs = (& "$DownloadLocation\smartctl.exe" --scan -j | ConvertFrom-Json).devices
$HDDInfo = foreach ($HDD in $HDDs) {
    (& "$DownloadLocation\smartctl.exe" -t short -a -j $HDD.name) | convertfrom-json
}
$DiskHealth = @{}
#Checking SMART status
$SmartFailed = $HDDInfo | Where-Object { $_.Smart_Status.Passed -ne $true }
if ($SmartFailed) { $DiskHealth.add('SmartErrors',"Smart Failed for disks: $($SmartFailed.serial_number)") }
#checking Temp Status
$TempFailed = $HDDInfo | Where-Object { $_.temperature.current -ge $Temperature }
if ($TempFailed) { $DiskHealth.add('TempErrors',"Temperature failed for disks: $($TempFailed.serial_number)") }
#Checking Power Cycle Count status
$PCCFailed = $HDDInfo | Where-Object { $_.Power_Cycle_Count -ge $PowerCycles }
if ($PCCFailed ) { $DiskHealth.add('PCCErrors',"Power Cycle Count Failed for disks: $($PCCFailed.serial_number)") }
#Checking Power on Time Status
$POTFailed = $HDDInfo | Where-Object { $_.Power_on_time.hours -ge $PowerOnTime }
if ($POTFailed) { $DiskHealth.add('POTErrors',"Power on Time for disks failed : $($POTFailed.serial_number)") }

if (!$DiskHealth) { $DiskHealth = "Healthy" }

And that’s it! as always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring internet speeds

It seems like I’m having a week of requests. This one was requested by my friends at Datto. One of their clients wanted to have the ability to run speed-tests and have their RMM system generate alerts whenever the speed drops. I’ve made the following PowerShell script that uses the CLI utility from speedtest.net. This utility gives us some nice feedback to work with;

  • It returns the external IP & the internal IP for the interface used.
  • The current ISP.
  • The download and upload speed.
  • The Jitter,Latency, and packet loss of the connection.
  • and the server it uses, plus the actual speedtest.net URL so you can compare the results.

So, I’ve made the script use two different monitoring methods; one is absolute and based and the values you’ve entered. The other is a percentage based monitor that alerts if the difference between the current speedtest and the previous one is more than 20%.

The script

The script downloads the Speedtest utility from the speedtest website. You can always replace this URL with your own host if you’d like.

######### Absolute monitoring values ########## 
$maxpacketloss = 2 #how much % packetloss until we alert. 
$MinimumDownloadSpeed = 100 #What is the minimum expected download speed in Mbit/ps
$MinimumUploadSpeed = 20 #What is the minimum expected upload speed in Mbit/ps
######### End absolute monitoring values ######

#Replace the Download URL to where you've uploaded the ZIP file yourself. We will only download this file once. 
#Latest version can be found at: https://www.speedtest.net/nl/apps/cli
$DownloadURL = "https://bintray.com/ookla/download/download_file?file_path=ookla-speedtest-1.0.0-win64.zip"
$DownloadLocation = "$($Env:ProgramData)\SpeedtestCLI"
try {
    $TestDownloadLocation = Test-Path $DownloadLocation
    if (!$TestDownloadLocation) {
        new-item $DownloadLocation -ItemType Directory -force
        Invoke-WebRequest -Uri $DownloadURL -OutFile "$($DownloadLocation)\speedtest.zip"
        Expand-Archive "$($DownloadLocation)\speedtest.zip" -DestinationPath $DownloadLocation -Force
    } 
}
catch {  
    write-host "The download and extraction of SpeedtestCLI failed. Error: $($_.Exception.Message)"
    exit 1
}
$PreviousResults = if (test-path "$($DownloadLocation)\LastResults.txt") { get-content "$($DownloadLocation)\LastResults.txt" | ConvertFrom-Json }
$SpeedtestResults = & "$($DownloadLocation)\speedtest.exe" --format=json --accept-license --accept-gdpr
$SpeedtestResults | Out-File "$($DownloadLocation)\LastResults.txt" -Force
$SpeedtestResults = $SpeedtestResults | ConvertFrom-Json

#creating object
[PSCustomObject]$SpeedtestObj = @{
    downloadspeed = [math]::Round($SpeedtestResults.download.bandwidth / 1000000 * 8, 2)
    uploadspeed   = [math]::Round($SpeedtestResults.upload.bandwidth / 1000000 * 8, 2)
    packetloss    = [math]::Round($SpeedtestResults.packetLoss)
    isp           = $SpeedtestResults.isp
    ExternalIP    = $SpeedtestResults.interface.externalIp
    InternalIP    = $SpeedtestResults.interface.internalIp
    UsedServer    = $SpeedtestResults.server.host
    ResultsURL    = $SpeedtestResults.result.url
    Jitter        = [math]::Round($SpeedtestResults.ping.jitter)
    Latency       = [math]::Round($SpeedtestResults.ping.latency)
}
$SpeedtestHealth = @()
#Comparing against previous result. Alerting is download or upload differs more than 20%.
if ($PreviousResults) {
    if ($PreviousResults.download.bandwidth / $SpeedtestResults.download.bandwidth * 100 -le 80) { $SpeedtestHealth += "Download speed difference is more than 20%" }
    if ($PreviousResults.upload.bandwidth / $SpeedtestResults.upload.bandwidth * 100 -le 80) { $SpeedtestHealth += "Upload speed difference is more than 20%" }
}

#Comparing against preset variables.
if ($SpeedtestObj.downloadspeed -lt $MinimumDownloadSpeed) { $SpeedtestHealth += "Download speed is lower than $MinimumDownloadSpeed Mbit/ps" }
if ($SpeedtestObj.uploadspeed -lt $MinimumUploadSpeed) { $SpeedtestHealth += "Upload speed is lower than $MinimumUploadSpeed Mbit/ps" }
if ($SpeedtestObj.packetloss -gt $MaxPacketLoss) { $SpeedtestHealth += "Packetloss is higher than $maxpacketloss%" }

if (!$SpeedtestHealth) {
    $SpeedtestHealth = "Healthy"
}

And that is it! So this monitoring component will be uploaded to the ComStore soon. As always, Happy PowerShelling!