homotechsual.dev
Open in
urlscan Pro
2a06:98c1:3121::3
Public Scan
Submitted URL: http://homotechsual.dev/
Effective URL: https://homotechsual.dev/
Submission: On March 27 via api from US — Scanned from NL
Effective URL: https://homotechsual.dev/
Submission: On March 27 via api from US — Scanned from NL
Form analysis
0 forms found in the DOMText Content
Skip to main content All original scripts published on this site are licensed under a Creative Commons Attribution-ShareAlike 4.0 International License DocumentationArchive Sponsor GitHub Projects * Blog Scripts * MSGraphMail * HaloAPI * NinjaOne ctrlK Recent posts * Initialise PSResourceGet (PowerShellGetv3) and PackageManagement * CVE Detection / Monitoring with NinjaOne Custom Fields * Deploying New Teams with PowerShell * Disabling (and Clearing) Browser Password Managers with PowerShell * Monitoring Time Drift with PowerShell * Targeting Windows Versions for Feature Updates * Deploying Printix Client with NinjaOne * Downloading CVE-2022-41099 Patch and SSU files * Sending Toast Notifications in Windows 10 and 11 * Updating Drivers from Microsoft Update INITIALISE PSRESOURCEGET (POWERSHELLGETV3) AND PACKAGEMANAGEMENT March 26, 2024 · One min read Mikey O'Toole PowerShell 5.1 as shipped with Windows 10 and 11 includes versions 1.0.0.1 of PackageManagement and PowerShellGet this old version cannot install most modern modules, nor can it self update properly. In most cases fixing this runs into numerous issues with conflicting versions or files in use. This script is an adaptation of a script by Chris Taylor which takes a different approach to downloading the modules, has a bit more error checking and further installs the new PSResourceGet module which is the replacement for PowerShellGet. THE SCRIPT Initialise-PowerShellGet.ps1 function Initialise-PSResourceGet { <# .SYNOPSIS Fixes common issues with PowerShellGet and PackageManagement and then installs PSResourceGet. .DESCRIPTION This script will check for common issues with PowerShellGet and PackageManagement and then install PSResourceGet. .NOTES Version: 2.0 Author: Mikey O'Toole Creation Date: 2024/03.26 Purpose/Change: Updated to also install PSResourceGet aka PowerShellGet v3. Now downloads the PackageManagmenet and PowerShellGet modules from the PowerShell Gallery directly rather than a zip on GitHub. -------------------------------------------------------------------- Version: 1.0 Author: Chris Taylor Creation Date: 2020/01/20 Purpose/Change: Initial script development #> [cmdletbinding()] Param( [System.IO.DirectoryInfo]$StagingPath = 'C:\RMM\PowerShellStaging\' ) $NuGetMinVersion = [System.Version]'3.0.0.1' $PackageManagementMinVersion = [System.Version]'1.4.8' $GalleryURL = 'https://www.powershellgallery.com/api/v2/' function Register-PSGallery { if ($Host.Version.Major -gt 4) { Register-PSRepository -Default } else { Import-Module PowerShellGet Register-PSRepository -Name PSGallery -SourceLocation $GalleryURL -InstallationPolicy Trusted } } function Redo-PowerShellGet { Write-Verbose 'Issue with PowerShellGet, Reinstalling.' $Module = 'PowerShellGet' foreach ($ProfilePath in $env:PSModulePath.Split(';')) { $FullPath = Join-Path $ProfilePath $Module Get-ChildItem $FullPath -Exclude '1.0.0.1' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force } Register-PSGallery $null = Install-Module $Module -Force -AllowClobber Import-Module $Module -Force } function Invoke-DownloadedModuleCleaner ([String]$ModulePath) { if (!(Test-Path -Path $ModulePath)) { throw ('Module path {0} does not exist.' -f $ModulePath) } Get-ChildItem -Path $ModulePath -Filter '*.nuspec' -Recurse | Remove-Item -Force -Recurse Get-ChildItem -Path $ModulePath -Filter '[Content_Types].xml' -Recurse | Remove-Item -Force -Recurse Get-ChildItem -Path $ModulePath -Filter '_rels' -Recurse -Directory | Remove-Item -Force -Recurse Get-ChildItem -Path $ModulePath -Filter 'package' -Recurse -Directory | Remove-Item -Force -Recurse } function Save-ModuleFromGallery ([String[]]$ModuleNames) { foreach ($ModuleName in $ModuleNames) { $Module = Invoke-RestMethod -Uri ("{0}/FindPackagesById()?id='{1}'&`$filter=IsLatestVersion and Id eq '{1}'" -f $GalleryURL, $ModuleName) -ErrorAction Stop $ModuleVersion = $Module.Properties.NormalizedVersion $ModuleURL = ('https://www.powershellgallery.com/api/v2/package/{0}/{1}' -f $ModuleName, $ModuleVersion) $WebClient = [System.Net.WebClient]::new() $ModuleFileName = ('{0}-{1}.zip' -f $ModuleName, $ModuleVersion) $ModuleDownloadPath = Join-Path -Path $StagingPath -ChildPath $ModuleFileName $WebClient.DownloadFile($ModuleURL, $ModuleDownloadPath) $ModuleExtractPath = Join-Path -Path $StagingPath -ChildPath $ModuleName $ModuleVersionedExtractPath = Join-Path -Path $ModuleExtractPath -ChildPath $ModuleVersion Expand-Archive -Path $ModuleDownloadPath -Destination $ModuleVersionedExtractPath -Force Invoke-DownloadedModuleCleaner -ModulePath $ModuleVersionedExtractPath } } if (!(Test-Path -Path $StagingPath)) { New-Item -Path $StagingPath -ItemType Directory | Out-Null } if ($PSVersionTable.PSVersion.Major -lt 3) { Write-Error 'Requires PowerShell version 3 or greater.' -ErrorAction Stop } try { [version]$DotNetVersion = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name Version).Version if ($DotNetVersion -lt [version]4.5) { throw } } catch { Write-Error '.NET version 4.5 or greater is needed.' -ErrorAction Stop } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { Write-Error 'TLS 1.2 Not supported.' -ErrorAction Stop } $WinmgmtService = Get-Service Winmgmt if ($WinmgmtService.StartType -eq 'Disabled') { Set-Service Winmgmt -StartupType Manual } if ($ENV:PSModulePath -split ';' -notcontains "$ENV:ProgramFiles\WindowsPowerShell\Modules") { [Environment]::SetEnvironmentVariable( 'PSModulePath', ((([Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') -split ';') + "$ENV:ProgramFiles\WindowsPowerShell\Modules") -join ';'), 'Machine' ) } # Remove Package Management Preview Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/X', '"{57E5A8BB-41EB-4F09-B332-B535C5954A28}"', '/qn') # Set Execution Policy Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Confirm:$false -Force -ErrorAction SilentlyContinue try { $null = Get-Command Install-PackageProvider -ErrorAction Stop $null = Get-Command Install-Module -ErrorAction Stop $PackageManagement = Get-Module PackageManagement -ListAvailable -ErrorAction Stop | Sort-Object Version -Descending | Select-Object -First 1 if ($PackageManagement.Version -lt $PackageManagementMinVersion) { throw } } catch { Write-Verbose 'Missing Package Manager, installing' $NeededModules = 'PowerShellGet', 'PackageManagement' Save-ModuleFromGallery -ModuleNames $NeededModules foreach ($Module in $NeededModules) { Write-Verbose ('Processing {0}' -f $Module) $DownloadedModulePath = Join-Path -Path $StagingPath -ChildPath $Module $ModulePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules' $InstalledModulePath = Join-Path -Path $ModulePath -ChildPath $Module if ($Host.Version.Major -lt 5) { # These versions of PoSh want the files in the root of the drive not version sub folders Write-Verbose ('Removing {0}' -f $InstalledModulePath) Remove-Module -Name $Module -Force -ErrorAction SilentlyContinue Remove-Item -Path $InstalledModulePath -Recurse -Force Write-Verbose ('Copying {0} to {1}' -f $DownloadedModulePath, $ModulePath) Get-ChildItem -Path $DownloadedModulePath | Get-ChildItem -Recurse | ForEach-Object { Copy-Item -Path $_.FullName -Destination $DownloadedModulePath -Force } } else { Write-Verbose ('Removing {0}' -f $InstalledModulePath) # If the folder name matches our target version don't remove it. Remove-Module -Name $Module -Force -ErrorAction SilentlyContinue Remove-Item -Path $InstalledModulePath -Recurse -Force New-Item -Path $InstalledModulePath -ItemType Directory -ErrorAction SilentlyContinue Write-Verbose ('Copying {0} to {1}' -f $DownloadedModulePath, $ModulePath) Copy-Item -Path $DownloadedModulePath -Destination $ModulePath -Recurse -Force } } Remove-Item $StagingPath -Force -Recurse -ErrorAction SilentlyContinue foreach ($Module in $NeededModules) { $Found = $false $ModulePaths = $ENV:PSModulePath -split ';' foreach ($ModulePath in $ModulePaths) { $Path = (Join-Path -Path $ModulePath -ChildPath $Module) if ((Test-Path $Path)) { $Found = $true $FoundModulePath = Get-ChildItem $Path -Recurse | Where-Object { $_.Name -eq ('{0}.psd1' -f $Module) } | Select-Object -First 1 Import-Module $FoundModulePath.FullName } } if (!$Found) { Write-Error ('Unable to find {0}' -f $Module) -ErrorAction Stop } } } # Reset the modules in our environment. $null = Remove-Module -Name PackageManagement -Force -ErrorAction SilentlyContinue $null = Remove-Module -Name PowerShellGet -Force -ErrorAction SilentlyContinue $null = Import-Module PackageManagement -Force -ErrorAction SilentlyContinue $null = Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue # Ensure PowerShellGet is working. try { $null = Get-Command Get-PackageProvider -ErrorAction Stop } catch { Redo-PowerShellGet } # Ensure we have the Nuget provider and test `Update-Module` and `Install-Module` are working. try { $Nuget = Get-PackageProvider NuGet -ListAvailable -ErrorAction Stop | Where-Object { $_.Version -ge $NuGetMinVersion } try { Update-Module PowerShellGet -Force -Confirm:$false -ErrorAction Stop } catch { Install-Module PowerShellGet -Force -Confirm:$false } } catch { $null = Install-PackageProvider NuGet -MinimumVersion $NuGetMinVersion -Force -Confirm:$false $null = Install-Module PowershellGet -Force -Confirm:$false } # Ensure we have NuGet. if (!$Nuget) { $null = Install-PackageProvider NuGet -MinimumVersion $NuGetMinVersion -Force -Confirm:$false } # Ensure we have PSNuGet. try { $null = Get-PackageSource -Name PSNuGet -ErrorAction Stop } catch { $null = Register-PackageSource -Name PSNuGet -Location $GalleryURL -ProviderName NuGet -Force } # Ensure we have PSGallery. try { $null = Get-PSRepository 'PSGallery' -ErrorAction Stop } catch { if ($_.exception.message -eq 'Invalid class') { Redo-PowerShellGet } else { Write-Verbose 'Registering PSGallery.' $PSRepositoriesPath = (Join-Path -Path $ENV:LocalAppData -ChildPath 'Microsoft\windows\PowerShell\PowerShellGet\PSRepositories.xml') Remove-Item -Path $PSRepositoriesPath -ErrorAction SilentlyContinue Register-PSGallery } } # Ensure PSGallery is trusted. if ((Get-PSRepository 'PSGallery').InstallationPolicy -ne 'Trusted') { Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted } # Ensure we have PSResourceGet. try { $null = Get-Command Install-PSResource -ErrorAction Stop } catch { Install-Module -Name 'Microsoft.PowerShell.PSResourceGet' -Force -Confirm:$false } } Initialise-PSResourceGet -ErrorAction Stop View on GitHub USAGE .\Initialise-PowerShellGet.ps1 Nice and simple on this one, just run the script and it will do the rest. It does expect a close-to-vanilla install of Windows 10 or 11, so if you've been messing around with the default modules before running this script, it may not work as expected. Tags: * CVE * Security * Vulnerability * NinjaOne * Custom Fields * PowerShell CVE DETECTION / MONITORING WITH NINJAONE CUSTOM FIELDS February 13, 2024 · 9 min read Mikey O'Toole This post will hold detection scripts for any serious CVE vulnerability that we write detection scripts for in the future. It will be updated and added to as new vulnerability detection scripts are written. CVE-2022-41099 This script has been compiled using information from the following Microsoft sources: * CVE-2022-41099: Security Update Guide * Add an update package to Windows RE security This article relates to CVE-2022-41099 which is a vulnerability in the Windows Recovery Environment (WinRE) which could allow a successful attacker to bypass the BitLocker Device Encryption feature on the system storage device. An attacker with physical access to the target could exploit this vulnerability to gain access to encrypted data. Fixed a Bug Thanks to DTGBilly from the NinjaOne Users Discord for pointing out that in altogether far too many places I had typo'd the CVE as CVE-4022-41099 instead of CVE-2022-41099 🤦♂️ this included field names and labels so please check yours are correct as now shown in the post. Parameters! Since version 1.2.0 (2023-03-21) this script now requires one of two mandatory parameters. * If you are checking for the presence of the small "Safe OS Dynamic Update (SODU)" which is the minimum required change to mitigate the vulnerability use the -CheckPackage parameter and if required alter the -MountDirectory and -LogDirectory parameters (defaults to C:\RMM\WinRE). * If you are checking for the presence of the larger "Servicing Stack Update (SSU)" or "Dynamic Cumulative Update" which updates more than is required to mitigate the vulnerability, but may offer other benefits including new WinRE functionality or more reliable reset/restore behaviours use the -CheckImage parameter which checks the image build version. If you were passing these in NinjaOne your parameter preset might look like this: -CheckPackage -MountDirectory C:\RMM\WinRE -LogDirectory C:\RMM\WinRE or this: -CheckImage Windows Recovery Environment (WinRE) Not Enabled Before version 1.3.0 the script did not check if WinRE was enabled which could lead to confusing error output in the event WinRE was disabled. Now if you get the WinRE not enabled warning you are clear on why the script isn't executing. A simple reagentc /enable should enable WinRE or at least provide some useful troubleshooting output. CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding one role custom field for devices with the Windows Desktop or Laptop and Windows Server roles, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately. Field LabelField NameField TypeDescriptionCVE-2022-41099CVE202241099CheckboxWhether the device has a WinRE image vulnerable to CVE-2022-41099 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/04/13. Detect-CVE202241099.ps1 <# .SYNOPSIS CVE Detection - CVE-2022-41099 .DESCRIPTION This script mounts the Windows Recovery Environment (WinRE) image and tests whether it is vulnerable to CVE-2022-41099 or whether it is of sufficient version or contains the correct packages to be considered patched against the CVE. .NOTES 2023-04-13: Checks for the vulnerability status are now more explicity and the script will equivocate if it cannot determine the status. 2023-03-24: Now checks whether Windows Recovery Environment (WinRE) is enabled before attempting to check the WinRE image. 2023-03-21: Major refactor, now supports checking for the presence of the SafeOS Dynamic Update packages. 2023-01-17: Better logic, more versions of Windows supported. 2023-01-13: Initial version .LINK Blog post: https://homotechsual.dev/2023/03/15/CVE-Monitoring-NinjaOne/ #> [CmdletBinding()] param ( [Parameter(ParameterSetName = 'Package', Mandatory = $true)] [Switch]$CheckPackage, [Parameter(ParameterSetName = 'Image', Mandatory = $true)] [Switch]$CheckImage, [Parameter(ParameterSetName = 'Package')] [System.IO.DirectoryInfo]$MountDirectory = 'C:\RMM\WinRE\Mount', [Parameter(ParameterSetName = 'Package')] [System.IO.DirectoryInfo]$LogDirectory = 'C:\RMM\WinRE\Logs' ) $WinREEnabled = (reagentc /info | findstr 'Enabled').Replace('Windows RE status: ', '').Trim() if (-not ($WinREEnabled)) { Write-Warning 'Windows RE is disabled - exiting...' return $false } $WinREImagePath = (reagentc /info | findstr '\\?\GLOBALROOT\device').Replace('Windows RE location: ', '').Trim() + '\winre.wim' $WinREBuild = (Get-WindowsImage -ImagePath $WinREImagePath -Index 1).SPBuild # $WinREModified = (Get-WindowsImage -ImagePath $WinREImagePath -Index 1).ModifiedTime $WinOSBuild = [System.Environment]::OSVersion.Version.Build $BuildtoKBMap = @{ 22623 = 5023527 22621 = 5023527 22000 = 5021040 19045 = 5021043 19044 = 5021043 19043 = 5021043 19042 = 5021043 } function Mount-WinRE { if (-not(Test-Path $MountDirectory)) { New-Item $MountDirectory -ItemType Directory } else { $MountDirectoryContents = Get-ChildItem $MountDirectory if ($MountDirectoryContents) { Write-Warning "Mount directory isn't empty - exiting..." return $false } } if ((Get-WindowsImage -Mounted).count -ge 1) { Write-Warning 'There is at least one other image mounted already = exiting...' return $false } $Mount = ReAgentC.exe /mountre /path $MountDirectory if ($Mount) { if ($Mount[0] -notmatch '.*\d+.*' -and (Get-WindowsImage -Mounted).count -ge 1 -and $LASTEXITCODE -eq 0) { return $true } } else { Write-Warning 'Could not mount WinRE image.' Write-Warning "$Mount" return $false } } function Dismount-WinRE { $DismountImageLogFile = Join-Path -Path $LogDirectory -ChildPath ('Dismount-WindowsImage_{0}.log' -f $DateTime) $DismountWinRECommonParameters = @{ Path = $MountDirectory LogLevel = 'WarningsInfo' } $UnmountDiscard = ReAgentC.exe /unmountre /path $($MountDirectory) /discard if (($UnmountDiscard[0] -match '.*\d+.*') -or $LASTEXITCODE -ne 0) { Write-Warning 'Attempting to unmount and discard failed - trying alternative method' Dismount-WindowsImage @DismountWinRECommonParameters -LogPath $DismountImageLogFile -Discard if ($(Get-WindowsImage -Mounted).count -ge 1) { Write-Warning 'Unmounting failed, including alternative methods.' return $false } else { return $true } } else { return $true } } if ($CheckPackage) { if (-not (Mount-WinRE)) { Write-Warning 'Could not mount WinRE image - exiting...' exit 1 } $KB = ('KB{0}' -f $BuildtoKBMap[$WinOSBuild]) $PackageApplied = (Get-WindowsPackage -Path $MountDirectory | Where-Object { $_.PackageName -like "*$KB*" }).PackageState -eq 'Installed' if (-not (Dismount-WinRE)) { Write-Warning 'Could not dismount WinRE image - exiting...' exit 1 } if (-not ($PackageApplied)) { Write-Warning 'SafeOS Dynamic Update Package not present in WinRE image.' $Vulnerable = $true } else { Write-Output 'SafeOS Dynamic Update Package present in WinRE image.' $Vulnerable = $false } } if ($CheckImage) { if (($WinOSBuild -in @(22623, 22621)) -and ($WinREBuild -lt 1105)) { $Vulnerable = $true } elseif (($WinOSBuild -eq 22000) -and ($WinREBuild -lt 1455)) { $Vulnerable = $true } elseif (($WinOSBuild -in @(19045, 19044, 19042)) -and ($WinREBuild -lt 2486)) { $Vulnerable = $true } elseif (($WinOSBuild -eq 19043) -and ($WinREBuild -lt 2364)) { $Vulnerable = $true } else { $Vulnerable = $false } } if ($true -eq $Vulnerable) { Write-Warning 'Vulnerable to CVE-2022-41099' Ninja-Property-Set CVE202241099 1 } elseif ($false -eq $Vulnerable) { Write-Output 'Not vulnerable to CVE-2022-41099' Ninja-Property-Set CVE202241099 0 } else { Write-Warning 'Could not determine vulnerability status.' } View on GitHub THE RESULTS We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. You'll find information on remediating this vulnerability in this followup post. CVE-2023-23397 This script has been compiled using information from the following Microsoft sources: * Release notes for Microsoft Office security updates * Update history for Microsoft 365 Apps (by date) * Update history for Office Beta Channel Thanks to: * Concentus on the NinjaOne Users Discord for helping me run down and test different versions of Office to ensure this script was as accurate as possible. * Wisecompany on the One Man Band MSP Discord for reminding me to add an exit code and not overuse Write-Warning! * Thanks to KennyW on the MSPGeek Discord for helping find an error where certain versions were incorrectly detected as not vulnerable! * Thanks to Alkerayn on the NinjaOne Users Discord for helping find an error where certain channels were incorrectly detected as not vulnerable and identifying that we needed to first check the GPO-configured update channel! * Thanks to Tanner - MO on the MSPs R Us Discord for pointing out that version comparisons should all use -lt instead of -ne to ensure future compatibility / accuracy. * Thanks to DarrenWhite99 on the MSPGeek Discord for pointing out that the check for the GPO UpdateChannel was completely nonsensical and incompletely written. * Thanks to JSanz on the NinjaOne Users Discord for pointing out the GUID matching issue/bug. * Thanks to Jhn - TS on the NinjaOne Users Discord for discovering the issue with empty registry props causing the script to error. This has only been tested against M365 Apps and Office 2021 VL versions "en masse" and only 64-bit office - if it doesn't work for you let me know on the NinjaOne Users Discord and I'll see what I can do to fix it! security This article relates to CVE-2023-23397 which is a vulnerability in Microsoft Outlook whereby an attacker could access a user's Net-NTLMv2 hash which could be used as a basis of an NTLM Relay attack against another service to authenticate as the user. CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding one role custom field for devices with the Windows Desktop or Laptop role, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately. Field LabelField NameField TypeDescriptionCVE-2023-23397CVE202323397CheckboxWhether the device has an Office or Microsoft 365 Apps version vulnerable to CVE-2023-23397. 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/04/13. Detect-CVE202323397.ps1 <# .SYNOPSIS CVE Detection - CVE-2023-23397.ps1 .DESCRIPTION This script checks the installed version of Office "Click-to-Run" installations to ascertain whether the installed version is vulnerable to CVE-2023-23397 which affects Outlook for Windows. .NOTES 2023-04-13: Checks for the vulnerability status are now more explicity and the script will equivocate if it cannot determine the status. 2023-03-17: Fix URL handling to avoid errors when the key exists with a null value. 2023-03-17: Improved output, fixed a bug where the update channel GUID was failing to match despite actually matching! 2023-03-17: Silently continue if missing registry properties. 2023-03-17: Handle more Office update channel configuration locations. Fix incorrect channel detection logic when using the GPO UpdateChannel. 2023-03-16: Check versions using a "less than" comparison for vulnerability to allow future proof usage. 2023-03-16: Check GPO channel config, adjust target version for Semi-Annual Enterprise (Preview) channel, fix Write-Warning/Write-Output mixup, more output info. 2023-03-16: Fix O365 app misdetection, better error handling, don't omit warning on success. 2023-03-15: Initial version .LINK Blog post: https://homotechsual.dev/2023/03/15/CVE-Monitoring-NinjaOne/ #> $IsC2R = Test-Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun' if ($IsC2R) { # Get the installed Office Version $OfficeVersion = [version]( Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' | Select-Object -ExpandProperty VersionToReport ) # Get the installed Office Product IDs $OfficeProductIds = ( Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' | Select-Object -ExpandProperty ProductReleaseIds ) } else { Write-Error 'No Click-to-Run Office installation detected. This script only works with Click-to-Run Office installations.' Exit 1 } $IsO365 = $OfficeProductIds -like '*O365*' if ($IsO365) { # Check the Office GPO settings for the update channel. $OfficeUpdateChannelGPO = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Office\16.0\Common\OfficeUpdate' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UpdateBranch -ErrorAction 'SilentlyContinue') if ($OfficeUpdateChannelGPO) { Write-Output 'Office is configured to use a GPO update channel.' # Define the Office Update Channels $Channels = @( @{ ID = 'Current' Name = 'Current' PatchedVersion = [version]'16.0.16130.20306' }, @{ ID = 'FirstReleaseCurrent' Name = 'Current (Preview)' PatchedVersion = [version]'16.0.16227.20094' }, @{ ID = 'MonthlyEnterprise' Name = 'Monthly Enterprise' PatchedVersion = [version]'16.0.16026.20238' }, @{ ID = 'Deferred' Name = 'Semi-Annual Enterprise' PatchedVersion = [version]'16.0.15601.20578' }, @{ # This does not match Microsoft's documented version but is the latest available update on tested SAE-Preview channel installations. ID = 'FirstReleaseDeferred' Name = 'Semi-Annual Enterprise (Preview)' PatchedVersion = [version]'16.0.16026.20238' }, @{ ID = 'InsiderFast' Name = 'Beta' PatchedVersion = [version]'16.0.16310.20000' } ) foreach ($Channel in $Channels) { if ($OfficeUpdateChannelGPO -eq $Channel.ID) { $OfficeChannel = $Channel } } } else { $C2RConfigurationPath = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' Write-Output 'Office is not configured to use a GPO update channel.' # Get the UpdateUrl if set $OfficeUpdateURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UpdateURL -ErrorAction 'SilentlyContinue') # Get the UnmanagedUpdateUrl if set $OfficeUnmanagedUpdateURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UnmanagedUpdateURL -ErrorAction 'SilentlyContinue') # Get the Office Update CDN URL $OfficeUpdateChannelCDNURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty CDNBaseUrl -ErrorAction 'SilentlyContinue') # Get just the channel GUID if ($OfficeUpdateURL.IsAbsoluteUri) { $OfficeUpdateGUID = $OfficeUpdateURL.Segments[2] } elseif ($OfficeUnmanagedUpdateURL.IsAbsoluteUri) { $OfficeUpdateGUID = $OfficeUnmanagedUpdateURL.Segments[2] } elseif ($OfficeUpdateChannelCDNURL.IsAbsoluteUri) { $OfficeUpdateGUID = $OfficeUpdateChannelCDNURL.Segments[2] } else { Write-Error 'Unable to determine Office update channel URL.' Exit 1 } # Define the Office Update Channels $Channels = @( @{ ID = '492350f6-3a01-4f97-b9c0-c7c6ddf67d60' Name = 'Current' PatchedVersion = [version]'16.0.16130.20306' }, @{ ID = '64256afe-f5d9-4f86-8936-8840a6a4f5be' Name = 'Current (Preview)' PatchedVersion = [version]'16.0.16227.20094' }, @{ ID = '55336b82-a18d-4dd6-b5f6-9e5095c314a6' Name = 'Monthly Enterprise' PatchedVersion = [version]'16.0.16026.20238' }, @{ ID = '7ffbc6bf-bc32-4f92-8982-f9dd17fd3114' Name = 'Semi-Annual Enterprise' PatchedVersion = [version]'16.0.15601.20578' }, @{ # This does not match Microsoft's documented version but is the latest available update on tested SAE-Preview channel installations. ID = 'b8f9b850-328d-4355-9145-c59439a0c4cf' Name = 'Semi-Annual Enterprise (Preview)' PatchedVersion = [version]'16.0.16026.20238' }, @{ ID = '5440fd1f-7ecb-4221-8110-145efaa6372f' Name = 'Beta' PatchedVersion = [version]'16.0.16310.20000' } ) foreach ($Channel in $Channels) { if ($OfficeUpdateGUID -eq $Channel.ID) { $OfficeChannel = $Channel } } } if (-not $OfficeChannel) { Write-Error 'Unable to determine Office update channel.' Exit 1 } else { Write-Output ("{0} found using the {1} update channel. `r`nChannel ID: {2}. `r`nTarget Version: {3}. `r`nDetected Version: {4}" -f 'Microsoft 365 Apps', $OfficeChannel.Name, $OfficeChannel.ID, $OfficeChannel.PatchedVersion, $OfficeVersion) } } if ( $OfficeVersion.Major -eq '16' ) { if ( ( $OfficeVersion.Build -ge 7571 ) -and ( $OfficeVersion.Build -le 16130 ) -and $IsO365 ) { # Handle Microsoft 365 Apps if ($OfficeVersion -lt $OfficeChannel.PatchedVersion) { $Vulnerable = $true } } elseif ( ( $OfficeVersion.Build -ge 10356) -and ( $OfficeVersion.Build -le 10396 ) -and ( $OfficeProductIds -like '*2019Volume*' ) -and ( $OfficeProductIds -like '*2019Volume*' ) ) { # Handle VL Office 2019 if ( ( $OfficeVersion.Build -lt 10396 ) -and ( $OfficeVersion.Revision -lt 20023 ) ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2019 VL', [Version]'16.0.10396.20023', $OfficeVersion) $Vulnerable = $true } } elseif ( ( $OfficeVersion.Build -ge 12527 ) -and ( $OfficeVersion.Build -le 16130 ) -and ( $OfficeProductIds -like '*Retail*' ) ) { # Handle Office 2021 Retail, Office 2019 Retail and Office 2016 Retail if ( ( $OfficeVersion.Build -lt 16130 ) -and ( $OfficeVersion.Revision -lt 20306 ) ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2021, 2019 or 2016 Retail', [Version]'16.0.16130.20306', $OfficeVersion) $Vulnerable = $true } } elseif ( ( $OfficeVersion.Build -eq 14332 ) -and ( $OfficeProductIds -like '*2021Volume*' ) ) { # Handle VL Office LTSC 2021 if ( ( $OfficeVersion.Build -ne 14332 ) -and ( $OfficeVersion.Revision -lt 20481 ) ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office LTSC 2021', [Version]'16.0.14332.20481', $OfficeVersion) $Vulnerable = $true } } } elseif ( $OfficeVersion.Major -eq '15' ) { if ( [version]'15.0.5537.1000' -gt $OfficeVersion ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2013', [Version]'15.0.5537.1000', $OfficeVersion) $Vulnerable = $true } } if ($true -eq $Vulnerable) { Write-Warning 'This version of Office is vulnerable to CVE-2023-23397.' Ninja-Property-Set CVE202323397 1 } elseif ($false -eq $Vulnerable) { Write-Output 'This version of Office is not vulnerable to CVE-2023-23397.' Ninja-Property-Set CVE202323397 0 } else { Write-Warning 'Could not determine vulnerability status.' } View on GitHub THE RESULTS We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. To remediate this vulnerability update Microsoft Office by running something like this: 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/03/16. Update-C2ROffice.ps1 <# .SYNOPSIS Update Management - Update Click-to-Run Office .DESCRIPTION This script forces through an update of Click-to-Run installations of Microsoft Office or Microsoft 365 apps. .NOTES 2023-03-15: Exit if the Click-to-Run executable can't be found 2023-03-15: Initial version .LINK Blog post: https://homotechsual.dev/2023/03/15/CVE-Monitoring-NinjaOne/ #> [CmdletBinding()] param () if ([System.Environment]::Is64BitOperatingSystem) { $C2RPaths = @( (Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files (x86)\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe'), (Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe') ) } else { $C2RPaths = (Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe') } $C2RPaths | ForEach-Object { if (Test-Path -Path $_) { $C2RPath = $_ } } if ($C2RPath) { Write-Verbose "C2RPath: $C2RPath" Start-Process -FilePath $C2RPath -ArgumentList '/update user displaylevel=false forceappshutdown=true' -Wait } else { Write-Error 'No Click-to-Run Office installation detected. This script only works with Click-to-Run Office installations.' Exit 1 } View on GitHub This update script will force restart Office apps - it should restore open files automatically but if you want a softer approach replace the Start-Process line with: Start-Process -FilePath $C2RPath -ArgumentList '/update user forceappshutdown=true updatepromptuser=true' -Wait Prejay on the MSPGeek Discord has helpfully suggested the following to update C2R Office builds without a user logged in or as system: Start-Process -FilePath $C2RPath -ArgumentList '/frequentupdate SCHEDULEDTASK displaylevel=false' -Wait Mark Hodges (also on the MSPGeek Discord) has also helpfully suggested this more comprehensive update script which will update Office 2016 and 2019 as well as C2R Office. CVE-2023-21554 This script has been compiled using information from the following Microsoft sources: * CVE-2023-21554: Security Update Guide security This article relates to CVE-2023-21554 which is a vulnerability in the Microsoft Message Queuing system which could allow remote code execution. CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding one role custom field for devices with the Windows Desktop or Laptop role, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately. Field LabelField NameField TypeDescriptionCVE-2023-21554CVE202321554CheckboxWhether the device has the MSMQ features installed and is missing the April 2023 Security Update. 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/03/16. Detect-CVE202321554.ps1 <# .SYNOPSIS CVE Detection - CVE-2023-21554 .DESCRIPTION This script checks whether the Microsoft Message Queuing (MSMQ) service is installed and then checks whether the April 2023 security update KBs have been installed patching for CVE-2023-21554. .NOTES 2023-04-13: Change the comparison logic when testing if the update is installed to use `Compare-Object`. 2023-04-13: Initial version .LINK Blog post: https://homotechsual.dev/2023/03/15/CVE-Monitoring-NinjaOne/ #> [CmdletBinding()] param () # Prepare variables and data sources. $April2023SecurityUpdateKBs = @( 'KB5025285', 'KB5025288', 'KB5025287', 'KB5025272', 'KB5025279', 'KB5025277', 'KB5025271', 'KB5025228', 'KB5025234', 'KB5025221', 'KB5025239', 'KB5025224', 'KB5025230' ) $MSMQServices = Get-WindowsOptionalFeature -FeatureName 'MSMQ*' -Online | Where-Object -Property 'State' -EQ 'Enabled' $InstalledKBs = [System.Collections.Generic.List[string]]::New() $Hotfixes = Get-HotFix | Select-Object -ExpandProperty HotFixID $WUSession = New-Object -ComObject 'Microsoft.Update.Session' $WUSearcher = $WUSession.CreateUpdateSearcher() $WUHistoryCount = $WUSearcher.GetTotalHistoryCount() # Logic loops if (-not ($MSMQServices)) { Write-Output 'MSMQ services not installed' $Vulnerable = $false } if ($null -eq $Vulnerable) { if ($Hotfixes.count -gt 0) { foreach ($Hotfix in $Hotfixes) { $InstalledKBs.Add($Hotfix) } } if ($WUHistoryCount -gt 0) { $UpdateHistory = $WUSearcher.QueryHistory(0, $WUHistoryCount) | ForEach-Object { [regex]::match($_.Title, '(KB\d+)').Value } $UpdateHistory = $UpdateHistory | Where-Object { $_ -Match '\S' } | Sort-Object -Unique foreach ($Update in $UpdateHistory) { if ($Update.HistoryID -match 'KB\d+') { $InstalledKBs.Add($Matches[0]) } } } Write-Output $InstalledKBs $InstalledAprilSecurityUpdates = Compare-Object -ReferenceObject $April2023SecurityUpdateKBs -DifferenceObject $InstalledKBs -IncludeEqual -ExcludeDifferent if ($InstalledAprilSecurityUpdates) { Write-Output ('Found April 2023 security update.') $Vulnerable = $false } else { $Vulnerable = $true } } if ($true -eq $Vulnerable) { Write-Warning 'Vulnerable to CVE-2023-21554' Ninja-Property-Set CVE202321554 1 } elseif ($false -eq $Vulnerable) { Write-Output 'Not vulnerable to CVE-2023-21554' Ninja-Property-Set CVE202321554 0 } else { Write-Warning 'Could not determine vulnerability status.' } View on GitHub THE RESULTS We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. To remediate install the April 2023 Security Update. CVE-2023-35628 This script has been compiled using information from the following Microsoft sources: * CVE-2023-35628: Security Update Guide security This article relates to CVE-2023-35628 which is a vulnerability affecting Microsoft Outlook's email rendering system which could allow remote code execution. CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding one role custom field for devices with the Windows Desktop or Laptop and/or Windows Server role, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately. Thanks to Gavsto for stopping me doing down the rabbit hole of checking KB numbers by pointing out that it wouldn't be future proof once the next cumulative update was released! Field LabelField NameField TypeDescriptionCVE-2023-35628CVE202335628CheckboxWhether the device is updated/patched for CVE-2023-35628. THE SCRIPT Detect-CVE202335628.ps1 <# .SYNOPSIS CVE Detection - CVE-2023-35628 .DESCRIPTION This script checks whether the Microsoft Outlook or Microsoft Outlook for Windows apps are installed and then checks whether the December 2023 cumulative update has been installed patching for CVE-2023-35628. .NOTES 2023-12-14: Initial version .LINK Blog post: https://homotechsual.dev/2023/03/15/CVE-Monitoring-NinjaOne/ #> [CmdletBinding()] param () # Prepare variables and data sources. $MinimumOSBuilds = [System.Collections.Generic.List[version]]@( '6.3.9600.21715', '6.2.9200.24614', '6.1.7601.26864', '10.0.25398.584', '10.0.22631.2861', '10.0.22621.2861', '10.0.22000.2652', '10.0.20348.2159', '10.0.20348.2144', '10.0.19045.3803', '10.0.19041.3803', '10.0.17763.5206', '10.0.14393.6529', '10.0.10240.20345' ) # Logic loops $OutlookClassesPresent = (Get-ItemProperty HKLM:\SOFTWARE\Classes\Outlook.Application -ErrorAction SilentlyContinue) $OutlookDesktopInstalled = (Get-Item -Path (Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files*\Microsoft Office\root\Office*\OUTLOOK.EXE') -ErrorAction SilentlyContinue) $OutlookNewInstalled = (Get-AppxPackage -AllUsers -Name 'Microsoft.OutlookForWindows' -ErrorAction SilentlyContinue) if ((-not $OutlookClassesPresent) -and (-not $OutlookDesktopInstalled) -and (-not $OutlookNewInstalled)) { Write-Output 'Outlook is probably not installed.' Ninja-Property-Set CVE202335628 0 return } else { Write-Warning 'Outlook is probably installed. Checking for CVE-2023-35628 patch.' } $OSVersion = [System.Environment]::OSVersion.Version $OSMajorMinorBuild = [Version]('{0}.{1}.{2}' -f $OSVersion.Major, $OSVersion.Minor, $OSVersion.Build) $MinimumApplicableOSBuilds = $MinimumOSBuilds | Where-Object { $_ -ge $OSMajorMinorBuild } $BuildsToTest = $MinimumApplicableOSBuilds | Where-Object { $_.Build -eq $OSMajorMinorBuild.Build } foreach ($Build in $BuildsToTest) { if ($Build -lt $OSVersion) { Write-Warning ('Minimum OS build requirement not met. Minimum OS build: {0}' -f $Build) $Vulnerable = $true } else { Write-Output ('Minimum OS build requirement met. Minimum OS build: {0}' -f $Build) $Vulnerable = $false } } if ($true -eq $Vulnerable) { Write-Warning 'Vulnerable to CVE-2023-35628' Ninja-Property-Set CVE202335628 1 } elseif ($false -eq $Vulnerable) { Write-Output 'Not vulnerable to CVE-2023-35628' Ninja-Property-Set CVE202335628 0 } else { Write-Warning 'Could not determine vulnerability status.' } View on GitHub THE RESULTS We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. To remediate install the December 2023 Cumulative Update. CVE-2024-21413 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/02/16. This script has been compiled using information from the following Microsoft sources: * CVE-2024-21413: Security Update Guide security This article relates to CVE-2023-35628 which is a vulnerability affecting Microsoft Outlook's preview pane system which could allow remote code execution. The updates include changes/corrections to the targetted versions - check your version please! CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding one role custom field for devices with the Windows Desktop or Laptop and/or Windows Server role, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately. Field LabelField NameField TypeDescriptionCVE-2024-21413CVE202421413CheckboxWhether the device is updated/patched for CVE-2024-21413. THE SCRIPT Detect-CVE202421413.ps1 <# .SYNOPSIS CVE Detection - CVE-2024-21413 .DESCRIPTION This script checks whether the Microsoft Outlook is installed and then checks whether the February 2024 security update has been installed patching for CVE-2024-21413. .NOTES 2024-03-01: 2024-02-15: Fix incorrect target versions for some M365 channels. 2024-02-15: Fail early if we can't match to a valid M365 apps version. Correct target versions for Office 2016, 2019 and 2021. 2024-02-14: Fix incorrect M365 version build upper limit. 2024-02-13: Initial version .LINK Blog post: https://homotechsual.dev/2024/02/13/CVE-Monitoring-NinjaOne/ #> $IsC2R = Test-Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun' if ($IsC2R -contains $true) { # Get the installed Office Version $OfficeVersion = [version]( Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' | Select-Object -ExpandProperty VersionToReport ) # Get the installed Office Product IDs $OfficeProductIds = ( Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' | Select-Object -ExpandProperty ProductReleaseIds ) } else { Write-Error 'No Click-to-Run Office installation detected. This script only works with Click-to-Run Office installations.' Exit 1 } $IsO365 = $OfficeProductIds -like '*O365*' $M365AppsChannels = @( @{ ID = 'Current' GUID = '492350f6-3a01-4f97-b9c0-c7c6ddf67d60' Name = 'Current' 17231 = @{ PatchedVersion = [version]'16.0.17231.20236' } }, @{ ID = 'FirstReleaseCurrent' GUID = '64256afe-f5d9-4f86-8936-8840a6a4f5be' Name = 'Current (Preview)' 17328 = @{ PatchedVersion = [version]'16.0.17328.20068' } }, @{ ID = 'MonthlyEnterprise' GUID = '55336b82-a18d-4dd6-b5f6-9e5095c314a6' Name = 'Monthly Enterprise' 17126 = @{ PatchedVersion = [version]'16.0.17126.20190' } 17029 = @{ PatchedVersion = [version]'16.0.17029.20178' } }, @{ ID = 'Deferred' GUID = '7ffbc6bf-bc32-4f92-8982-f9dd17fd3114' Name = 'Semi-Annual Enterprise' 15601 = @{ PatchedVersion = [version]'16.0.15601.20870' } 16130 = @{ PatchedVersion = [version]'16.0.16130.20916' } 16731 = @{ PatchedVersion = [version]'16.0.16731.20550' } }, @{ ID = 'FirstReleaseDeferred' GUID = 'b8f9b850-328d-4355-9145-c59439a0c4cf' Name = 'Semi-Annual Enterprise (Preview)' 16731 = @{ PatchedVersion = [version]'16.0.16731.20550' } }, @{ ID = 'InsiderFast' GUID = '5440fd1f-7ecb-4221-8110-145efaa6372f' Name = 'Beta' 17404 = @{ PatchedVersion = [version]'16.0.17404.20000' } } ) if ($IsO365) { Write-Output 'Detected M365 Apps installation.' # Check the Office GPO settings for the update channel. $OfficeUpdateChannelGPO = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Office\16.0\Common\OfficeUpdate' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UpdateBranch -ErrorAction 'SilentlyContinue') if ($OfficeUpdateChannelGPO) { Write-Output 'Office is configured to use a GPO update channel.' foreach ($Channel in $M365AppsChannels) { if ($OfficeUpdateChannelGPO -eq $Channel.ID) { $OfficeChannel = $Channel } } } else { $C2RConfigurationPath = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' Write-Output 'Office is not configured to use a GPO update channel.' # Get the UpdateUrl if set $OfficeUpdateURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UpdateURL -ErrorAction 'SilentlyContinue') # Get the UnmanagedUpdateUrl if set $OfficeUnmanagedUpdateURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UnmanagedUpdateURL -ErrorAction 'SilentlyContinue') # Get the Office Update CDN URL $OfficeUpdateChannelCDNURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty CDNBaseUrl -ErrorAction 'SilentlyContinue') # Get just the channel GUID if ($OfficeUpdateURL.IsAbsoluteUri) { $OfficeUpdateGUID = $OfficeUpdateURL.Segments[2] } elseif ($OfficeUnmanagedUpdateURL.IsAbsoluteUri) { $OfficeUpdateGUID = $OfficeUnmanagedUpdateURL.Segments[2] } elseif ($OfficeUpdateChannelCDNURL.IsAbsoluteUri) { $OfficeUpdateGUID = $OfficeUpdateChannelCDNURL.Segments[2] } else { Write-Error 'Unable to determine Office update channel URL.' Exit 1 } # Define the Office Update Channels foreach ($Channel in $M365AppsChannels) { if ($OfficeUpdateGUID -eq $Channel.GUID) { $OfficeChannel = $Channel } } } if (-not $OfficeChannel) { Write-Error 'Unable to determine Office update channel.' Exit 1 } else { Write-Output ("{0} found using the {1} update channel. `r`nChannel ID: {2}. `r`nTarget Version: {3}. `r`nDetected Version: {4}" -f 'Microsoft 365 Apps', $OfficeChannel.Name, $OfficeChannel.ID, $OfficeChannel[$OfficeVersion.Build].PatchedVersion, $OfficeVersion) } } # Catch installations on builds older than supported if ( $IsO365 ) { if ($OfficeChannel[$OfficeVersion.Build].PatchedVersion ) { Write-Output 'Target version detected, continuing with script' } else { Write-Output 'No value for Targeted version, install is probably on an old build, marking as vunerable' Write-Warning 'This version of Office is vulnerable.' $Vulnerable = $true } } if ( $OfficeVersion.Major -eq '16' -and (!$Vulnerable) ) { if ( ( $OfficeVersion.Build -ge 7571 ) -and ( $OfficeVersion.Build -le 17404 ) -and $IsO365 ) { # Handle Microsoft 365 Apps if ($OfficeVersion -lt $OfficeChannel[$OfficeVersion.Build].PatchedVersion) { $Vulnerable = $true } } elseif ( ( $OfficeVersion.Build -ge 10356) -and ( $OfficeVersion.Build -le 10407 ) -and ( $OfficeProductIds -like '*2019Volume*' ) -and ( $OfficeProductIds -like '*2019Volume*' ) ) { # Handle VL Office 2019 if ( ( $OfficeVersion.Build -lt 10407 ) -and ( $OfficeVersion.Revision -lt 20032 ) ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2019 VL', [Version]'16.0.10407.20023', $OfficeVersion) $Vulnerable = $true } } elseif ( ( $OfficeVersion.Build -ge 12527 ) -and ( $OfficeVersion.Build -le 17231 ) -and ( $OfficeProductIds -like '*Retail*' ) ) { # Handle Office 2021 Retail, Office 2019 Retail and Office 2016 Retail if ( ( $OfficeVersion.Build -lt 16130 ) -and ( $OfficeVersion.Revision -lt 20236 ) ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2021, 2019 or 2016 Retail', [Version]'16.0.1.20306', $OfficeVersion) $Vulnerable = $true } } elseif ( ( $OfficeVersion.Build -eq 14332 ) -and ( $OfficeProductIds -like '*2021Volume*' ) ) { # Handle VL Office LTSC 2021 if ( ( $OfficeVersion.Build -ne 14332 ) -and ( $OfficeVersion.Revision -lt 20637 ) ) { Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office LTSC 2021', [Version]'16.0.14332.20637', $OfficeVersion) $Vulnerable = $true } } } if ($Vulnerable) { Write-Warning 'This version of Office is vulnerable to CVE-2024-21413.' Ninja-Property-Set CVE202421413 1 } else { Write-Output 'This version of Office is not vulnerable to CVE-2024-21413.' Ninja-Property-Set CVE202421413 0 } View on GitHub THE RESULTS We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. To remediate install the applicable Office / M365 Apps February 2024 Security Update. Tags: * CVE * Security * Vulnerability * NinjaOne * Custom Fields * PowerShell DEPLOYING NEW TEAMS WITH POWERSHELL January 10, 2024 · One min read Mikey O'Toole This post will show you how to deploy the New Teams client using a PowerShell script. THE SCRIPT Install-NewTeams.ps1 <# .SYNOPSIS Software Deployment - Generic - New Teams .DESCRIPTION Uses the Teams Bootstrapper executable to install New Teams machine-wide. Use the `-Offline` switch to install using the MSIX File. Use `-Uninstall` to remove machine-wide provisioning of New Teams. .NOTES 2024-02-07: I know what bloody year it is thanks to Geckojas... 2024-01-11: Fix incorrect syntax for if condition (thanks Jayrod) and silence progress bar for Invoke-WebRequest. 2024-01-10: Initial version .LINK Blog post: https://homotechsual.dev/2024/01/10/Deploy-New-Teams/ #> [CmdletBinding()] param( [System.IO.DirectoryInfo]$TeamsFolder = 'C:\RMM\Teams', [System.IO.FileInfo]$MSIXPath, [Switch]$Offline, [Switch]$Uninstall ) begin { $ProgressPreference = 'SilentlyContinue' # Make sure the folder exists to hold the downloaded files. if (-not (Test-Path -Path $TeamsFolder)) { New-Item -Path $TeamsFolder -ItemType Directory | Out-Null } # Source: https://learn.microsoft.com/en-us/microsoftteams/new-teams-bulk-install-client $TeamsInstallerDownloadId = 2243204 if ($Offline) { $TeamsMSIXDownloadIds = @{ 'x86' = 2196060 'x64' = 2196106 'ARM64' = 2196207 } $Is64Bit = [System.Environment]::Is64BitOperatingSystem $IsArm = [System.Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE') -like 'ARM*' if ($IsArm) { $TeamsMSIXDownloadId = $TeamsMSIXDownloadIds['ARM64'] } elseif ($Is64Bit) { $TeamsMSIXDownloadId = $TeamsMSIXDownloadIds['x64'] } else { $TeamsMSIXDownloadId = $TeamsMSIXDownloadIds['x86'] } $TeamsMSIXDownloadUri = ('https://go.microsoft.com/fwlink/p/?linkid={0}' -f $TeamsMSIXDownloadId) } $TeamsInstallerDownloadUri = ('https://go.microsoft.com/fwlink/p/?linkid={0}' -f $TeamsInstallerDownloadId) # Define our ProcessInvoker function. function ProcessInvoker ([System.IO.FileInfo]$CommandPath, [String[]]$CommandArguments) { $PSI = New-Object System.Diagnostics.ProcessStartInfo $PSI.FileName = (Resolve-Path $CommandPath) $PSI.RedirectStandardError = $true $PSI.RedirectStandardOutput = $true $PSI.UseShellExecute = $false $PSI.Arguments = $CommandArguments -join ' ' $Process = New-Object System.Diagnostics.Process $Process.StartInfo = $PSI $Process.Start() | Out-Null $ProcessOutput = [PSCustomObject]@{ STDOut = $Process.StandardOutput.ReadToEnd() STDErr = $Process.StandardError.ReadToEnd() ExitCode = $Process.ExitCode } $Process.WaitForExit() return $ProcessOutput } } process { # Download Teams installer $TeamsInstallerFile = Join-Path -Path $TeamsFolder -ChildPath 'TeamsBootstrapper.exe' Write-Verbose ('Downloading Teams installer to {0}' -f $TeamsInstallerFile) Invoke-WebRequest -Uri $TeamsInstallerDownloadUri -OutFile $TeamsInstallerFile if ($Offline -and (-not $MSIXPath)) { # Download Teams MSIX $TeamsMSIXFile = Join-Path -Path $TeamsFolder -ChildPath 'Teams.msixbundle' if (-not (Test-Path -Path $TeamsMSIXFile)) { Write-Verbose ('Downloading Teams MSIX to {0}' -f $TeamsMSIXFile) Invoke-WebRequest -Uri $TeamsMSIXDownloadUri -OutFile $TeamsMSIXFile } } elseif ($Offline -and $MSIXPath) { $TeamsMSIXFile = Resolve-Path $MSIXPath } if ($Offline) { # Install Teams MSIX with the bootstrapper Write-Verbose 'Installing Teams MSIX with the bootstrapper' $Output = ProcessInvoker -CommandPath $TeamsInstallerFile -CommandArguments @('-p', '-o', ('{0}' -f $TeamsMSIXFile)) } else { # Install Teams with the bootstrapper Write-Verbose 'Installing Teams with the bootstrapper' $Output = ProcessInvoker -CommandPath $TeamsInstallerFile -CommandArguments @('-p') -Wait -NoNewWindow } if ($Uninstall) { # Uninstall Teams Write-Verbose 'Uninstalling Teams' $Output = ProcessInvoker -CommandPath $TeamsInstallerFile -CommandArguments @('-x') -Wait -NoNewWindow } $OutputObject = $Output.STDOut | ConvertFrom-Json Write-Verbose $Output if ($OutputObject.Success) { Write-Verbose 'Teams installed successfully' } else { Write-Error ('Teams installation failed with error code {0}' -f $OutputObject.ErrorCode) } } end { $ProgressPreference = 'Continue' } View on GitHub Parameters When you run this script you need to pass some parameters. Use -Offline to install using the MSIX file (script will download this if you don't provide it) if you're providing the MSIX provide the path with -MSIXPath. Use -Uninstall to remove the provisioned New Teams package. You can specify your own staging folder with -TeamsFolder. Tags: * Software * Teams * Deployment * PowerShell DISABLING (AND CLEARING) BROWSER PASSWORD MANAGERS WITH POWERSHELL December 18, 2023 · 2 min read Mikey O'Toole When deploying a password manager, one of the first things you'll want to do is disable the built-in password manager in your browsers. This is a pretty simple task, but it's also one that's easy to forget. It's also a good idea to clear out any passwords that may have been saved before you deployed your password manager. We automate this for the two browsers we support on managed Windows devices (Edge and Firefox) using PowerShell. Here's how we do it. EDGE For Edge we're going to be setting the registry key at HKLM:\SOFTWARE\Policies\Microsoft\Edge\PasswordManagerEnabled to 0. This will disable the password manager for all users on the device. Then we're going to clear out any passwords that may have been saved by deleting the contents of the Login Data file in the user's Edge profile. We'll do this by removing the file entirely. Now in some cases you only want to do the first part (disabling the password manager) and not the second (clearing out any saved passwords). For that reason the script functionality is controlled with two switch parameters: -DisablePasswordManager and -RemoveExistingPasswords. If you run the script without either of these switches, it will do nothing. Application%20Configuration/EdgePasswordManagerConfig.ps1 <# .SYNOPSIS Application Configuration - Disable Edge Password Storage .DESCRIPTION This script disables the Edge password manager using the registry and then clears existing passwords by removing the contents of `$ENV:\SystemDrive\Users\*\AppData\Local\Microsoft\Edge\User Data\*\Login Data`. By necessity this script will force-end any running Edge processes. .EXAMPLE .\EdgePasswordManagerConfig.ps1 -RemoveExistingPasswords -DisablePasswordManager Disables the Edge password manager and removes any existing passwords. .EXAMPLE .\EdgePasswordManagerConfig.ps1 -RemoveExistingPasswords Removes any existing passwords. .EXAMPLE .\EdgePasswordManagerConfig.ps1 -DisablePasswordManager Disables the Edge password manager. .NOTES 2023-12-17: Initial version. .LINK Blog post: https://homotechsual.dev/2023/12/18/browser-password-manager-configuration/ #> [CmdletBinding()] param( [Parameter()] [switch]$RemoveExistingPasswords, [Parameter()] [switch]$DisablePasswordManager ) # Utility Function: Registry.ShouldBe ## This function is used to ensure that a registry value exists and is set to a specific value. function Registry.ShouldBe { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [string]$Value, [Parameter(Mandatory)] [ValidateSet('String','ExpandString','Binary','DWord','MultiString','QWord')] [string]$Type ) begin { # Make sure the registry path exists. if (!(Test-Path $Path)) { Write-Warning ("Registry path '$Path' does not exist. Creating.") New-Item -Path $Path -Force | Out-Null } # Make sure it's actually a registry path. if (!(Get-Item $Path).PSProvider.Name -eq 'Registry' -and !(Get-Item $Path).PSIsContainer) { throw "Path '$Path' is not a registry path." } } process { do { # Make sure the registry value exists. if (!(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) { Write-Warning ("Registry value '$Name' in path '$Path' does not exist. Setting to '$Value'.") New-ItemProperty -Path $Path -Name $Name -Value $Value -Force | Out-Null } # Make sure the registry value is correct. if ((Get-ItemProperty -Path $Path -Name $Name).$Name -ne $Value) { Write-Warning ("Registry value '$Name' in path '$Path' is not correct. Setting to '$Value'.") Set-ItemProperty -Path $Path -Name $Name -Value $Value } } while ((Get-ItemProperty -Path $Path -Name $Name).$Name -ne $Value) } } # Disable the Edge password manager. if ($DisablePasswordManager) { # Disable the Edge password manager. Write-Host "Disabling the Edge password manager." Registry.ShouldBe -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' -Name 'PasswordManagerEnabled' -Value 0 -Type DWord } # Remove existing passwords. if ($RemoveExistingPasswords) { # Get the Edge process(es). $EdgeProcesses = Get-Process -Name 'msedge' -ErrorAction SilentlyContinue # If there are any Edge processes, kill them. if ($EdgeProcesses) { Write-Host "Killing Edge processes." $EdgeProcesses | Stop-Process -Force } # Get the Edge user data directories. $UserPath = Join-Path -Path $ENV:SystemDrive -ChildPath 'Users' $UserProfiles = Get-ChildItem -Path $UserPath -Directory -ErrorAction SilentlyContinue $EdgePasswordFiles = foreach ($UserProfile in $UserProfiles) { $EdgeProfilePath = Join-Path -Path $UserProfile.FullName -ChildPath 'AppData\Local\Microsoft\Edge\User Data\' $EdgeStateFile = Join-Path $EdgeProfilePath -ChildPath 'Local State' $EdgeState = Get-Content -Path $EdgeStateFile -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json if ($EdgeState) { $EdgeProfiles = $EdgeState.profile.info_cache.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } | Select-Object -ExpandProperty Name foreach ($EdgeProfile in $EdgeProfiles) { $EdgeProfilePath = Join-Path -Path $UserProfile.FullName -ChildPath "AppData\Local\Microsoft\Edge\User Data\$EdgeProfile" $EdgePasswordFile = Join-Path -Path $EdgeProfilePath -ChildPath 'Login Data' if (Test-Path -Path $EdgePasswordFile) { $EdgePasswordFile } else { Write-Warning ('User {0} profile {1} does not have a password file.' -f $UserProfile.Name, $EdgeProfile) } } } } # If there are any Edge password files, remove the contents of the Login Data file. if ($EdgePasswordFiles) { Write-Host "Removing existing passwords." foreach ($EdgePasswordFile in $EdgePasswordFiles) { Remove-Item -Force -Path $EdgePasswordFile } } } View on GitHub FIREFOX For Firefox we're going to be setting the registry key at HKLM:\SOFTWARE\Policies\Mozilla\Firefox\PasswordManagerEnabled to 0. This will disable the password manager for all users on the device. Then we're going to clear out any passwords that may have been saved by deleting the contents of the logins.json file in the user's Firefox profile and any key*.db files. We'll do this by removing the files entirely. Now in some cases you only want to do the first part (disabling the password manager) and not the second (clearing out any saved passwords). For that reason the script functionality is controlled with two switch parameters: -DisablePasswordManager and -RemoveExistingPasswords. If you run the script without either of these switches, it will do nothing. Application%20Configuration/FirefoxPasswordManagerConfig.ps1 <# .SYNOPSIS Application Configuration - Disable Firefox Password Storage .DESCRIPTION This script disables the Firefox password manager using the registry and then clears existing passwords by removing the contents of `$ENV:\SystemDrive\Users\*\AppData\Local\Microsoft\Edge\User Data\*\Login Data`. By necessity this script will force-end any running Edge processes. .EXAMPLE .\FirefoxPasswordManagerConfig.ps1 -RemoveExistingPasswords -DisablePasswordManager Disables the Firefox password manager and removes any existing passwords. .EXAMPLE .\FirefoxPasswordManagerConfig.ps1 -RemoveExistingPasswords Removes any existing passwords. .EXAMPLE .\FirefoxPasswordManagerConfig.ps1 -DisablePasswordManager Disables the Firefox password manager. .NOTES 2023-12-18: Initial version. .LINK Blog post: https://homotechsual.dev/2023/12/18/browser-password-manager-configuration/ #> [CmdletBinding()] param( [Parameter()] [switch]$RemoveExistingPasswords, [Parameter()] [switch]$DisablePasswordManager ) # Utility Function: Registry.ShouldBe ## This function is used to ensure that a registry value exists and is set to a specific value. function Registry.ShouldBe { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [string]$Value, [Parameter(Mandatory)] [ValidateSet('String','ExpandString','Binary','DWord','MultiString','QWord')] [string]$Type ) begin { # Make sure the registry path exists. if (!(Test-Path $Path)) { Write-Warning ("Registry path '$Path' does not exist. Creating.") New-Item -Path $Path -Force | Out-Null } # Make sure it's actually a registry path. if (!(Get-Item $Path).PSProvider.Name -eq 'Registry' -and !(Get-Item $Path).PSIsContainer) { throw "Path '$Path' is not a registry path." } } process { do { # Make sure the registry value exists. if (!(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) { Write-Warning ("Registry value '$Name' in path '$Path' does not exist. Setting to '$Value'.") New-ItemProperty -Path $Path -Name $Name -Value $Value -Force | Out-Null } # Make sure the registry value is correct. if ((Get-ItemProperty -Path $Path -Name $Name).$Name -ne $Value) { Write-Warning ("Registry value '$Name' in path '$Path' is not correct. Setting to '$Value'.") Set-ItemProperty -Path $Path -Name $Name -Value $Value } } while ((Get-ItemProperty -Path $Path -Name $Name).$Name -ne $Value) } } # Disable the Firefox password manager. if ($DisablePasswordManager) { # Disable the Firefox password manager. Write-Host "Disabling the Firefox password manager." Registry.ShouldBe -Path 'HKLM:\SOFTWARE\Policies\Mozilla\Firefox' -Name 'PasswordManagerEnabled' -Value 0 -Type DWord } # Remove existing passwords. if ($RemoveExistingPasswords) { # Get the Firefox process(es). $FirefoxProcesses = Get-Process -Name 'firefox' -ErrorAction SilentlyContinue # If there are any Firefox processes, kill them. if ($FirefoxProcesses) { Write-Host "Killing Firefox processes." $FirefoxProcesses | Stop-Process -Force } # Get the Firefox user data directories. $UserPath = Join-Path -Path $ENV:SystemDrive -ChildPath 'Users' $UserProfiles = Get-ChildItem -Path $UserPath -Directory -ErrorAction SilentlyContinue $FirefoxPasswordFiles = foreach ($UserProfile in $UserProfiles) { $FirefoxProfilePath = Join-Path -Path $UserProfile.FullName -ChildPath 'AppData\Roaming\Mozilla\Firefox\Profiles\' $FirefoxProfiles = Get-ChildItem -Path $FirefoxProfilePath -Directory -ErrorAction SilentlyContinue foreach ($FirefoxProfile in $FirefoxProfiles) { $FirefoxLoginsFile = Join-Path -Path $FirefoxProfile.FullName -ChildPath 'logins.json' if (Test-Path -Path $FirefoxLoginsFile) { $FirefoxLoginsFile } else { Write-Warning ('User {0} profile {1} does not have a password file.' -f $UserProfile.Name, $FirefoxProfile.Name) } $FirefoxKeyFiles = Resolve-Path -Path (Join-Path -Path $FirefoxProfile.FullName -ChildPath 'key*') if ($FirefoxKeyFiles) { foreach ($FirefoxKeyFile in $FirefoxKeyFiles) { $FirefoxKeyFile } } else { Write-Warning ('User {0} profile {1} does not have a key file.' -f $UserProfile.Name, $FirefoxProfile.Name) } } } # If there are any Firefox password files, remove them. if ($FirefoxPasswordFiles) { Write-Host "Removing existing passwords." foreach ($FirefoxPasswordFile in $FirefoxPasswordFiles) { Remove-Item -Force -Path $FirefoxPasswordFile } } } View on GitHub As we don't use Chrome, Opera or Safari, we don't have scripts for those browsers. However for other chromium-based browsers a similar approach to Edge should work. It is possible to do this with Safari on MacOS as well but we haven't yet scripted it. Tags: * Configuration * Password Managers * Browsers * PowerShell MONITORING TIME DRIFT WITH POWERSHELL March 17, 2023 · 2 min read Mikey O'Toole Sometimes I get a script idea put in my head that's so irritatingly pervasive that the only fix is to write the damned script. David Szpunar from the NinjaOne Users Discord made a somewhat passing comment about time drift causing issues with a remote support tool and that let to me thinking... You could probably monitor for that with a PowerShell one-liner right? Wrong! Turns out that it's more than one line! 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 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/03/17. Test-TimeDrift.ps1 <# .SYNOPSIS Monitoring - Windows - Time Drift .DESCRIPTION This script will monitor time drift on the machine vs a provided "source of truth". .NOTES 2023-03-22: Exclude empty lines in the output. 2023-03-18: Add `-Resync` parameter to force a resync if the time drift exceeds threshold. 2023-03-17: Initial version .LINK Original Source: https://kevinholman.com/2017/08/26/monitoring-for-time-drift-in-your-enterprise/ .LINK Blog post: https://homotechsual.dev/2023/03/17/Monitoring-Time-Drift-PowerShell/ #> [CmdletBinding()] param ( # The NTP or local domain controller to use as a reference for time drift. [string]$ReferenceServer = 'time.windows.com', # The number of samples to take. [int]$NumberOfSamples = 1, # The allowed time drift in seconds. [int]$AllowedTimeDrift = 10, # Force a resync of the time if the time drift is greater than the allowed time drift. [switch]$ForceResync ) $Win32TimeExe = Join-Path -Path $ENV:SystemRoot -ChildPath 'System32\w32tm.exe' $Win32TimeArgs = '/stripchart /computer:{0} /samples:{1} /dataonly' -f $ReferenceServer, $NumberOfSamples $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo $ProcessInfo.FileName = $Win32TimeExe $ProcessInfo.Arguments = $Win32TimeArgs $ProcessInfo.RedirectStandardError = $true $ProcessInfo.RedirectStandardOutput = $true $ProcessInfo.UseShellExecute = $false $ProcessInfo.CreateNoWindow = $true $Process = New-Object System.Diagnostics.Process $Process.StartInfo = $ProcessInfo $Process.Start() | Out-Null $ProcessResult = [PSCustomObject]@{ ExitCode = $Process.ExitCode StdOut = $Process.StandardOutput.ReadToEnd() StdErr = $Process.StandardError.ReadToEnd() } $Process.WaitForExit() if ($ProcessResult.StdErr) { throw "w32tm.exe returned the following error: $($ProcessResult.StdErr)" } elseif ($ProcessResult.StdOut -contains 'Error') { throw "w32tm.exe returned the following error: $($ProcessResult.StdOut)" } else { Write-Debug ('Raw StdOut: {0}' -f $ProcessResult.StdOut) $ProcessOutput = $ProcessResult.StdOut.Split("`n") | Where-Object { $_ } $Skew = $ProcessOutput[-1..($NumberOfSamples * -1)] | ConvertFrom-Csv -Header @('Time', 'Skew') | Select-Object -ExpandProperty Skew Write-Debug ('Raw Skew: {0}' -f $Skew) $AverageSkew = $Skew | ForEach-Object { $_ -replace 's', '' } | Measure-Object -Average | Select-Object -ExpandProperty Average Write-Debug ('Average Skew: {0}' -f $AverageSkew) if ($AverageSkew -lt 0) { $AverageSkew = $AverageSkew * -1 } $TimeDriftSeconds = [Math]::Round($AverageSkew, 2) if ($TimeDriftSeconds -gt $AllowedTimeDrift) { if ($ForceResync) { Start-Process -FilePath $Win32TimeExe -ArgumentList '/resync' -Wait Write-Warning "Time drift was greater than the allowed time drift of $AllowedTimeDrift seconds. Time drift was $TimeDriftSeconds seconds A resync was forced." } else { throw "Time drift is greater than the allowed time drift of $AllowedTimeDrift seconds. Time drift is $TimeDriftSeconds seconds." } } else { Write-Verbose "Time drift is within accepted limits. Time drift is $TimeDriftSeconds seconds." } } View on GitHub USING THE SCRIPT We tested this as a "Script Result Condition" in NinjaOne set to trigger the monitor if a machine's time drifts by more than 10 seconds from uk.pool.ntp.org (the UK's NTP pool) and it worked like a charm. The script is pretty self-explanatory but here's a quick rundown of what it does: 1. It uses a configurable NTP or SNTP server to get the "reference" time. (Parameter -ReferenceServer) 2. It uses the w32tm executable to conduct a number of skew checks against that reference server (Parameter -NumberOfSamples) 3. It averages the samples and compares the result to the threshold (Parameter -AllowedTimeDrift) 4. Optionally you can force a resync if the time drift is greater than the threshold (Parameter -ForceResync) If the average time drift is greater than the threshold, the script returns a non-zero exit code and the monitor triggers. If the w32tm command errors (non existent server, network down etc) the script returns a non-zero exit code and the monitor triggers. CREDITS This script borrows ideas and the approach and a little code from the excellent blog of Kevin Holman. The formidable Chris Taylor helped with a cool suggestion to suppress empty lines in the output and his site is well worth a visit. Tags: * Monitoring * PowerShell TARGETING WINDOWS VERSIONS FOR FEATURE UPDATES February 16, 2023 · One min read Mikey O'Toole This post will show you how to use registry keys to test, set and remove target versions for Windows Feature Updates. This allows you to prevent Windows 10 or 11 from updating past your configured limit. 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/04/05. Invoke-WindowsUpdateTargetVersion.ps1 <# .SYNOPSIS Update Management - Windows - Set Feature Update Target Version .DESCRIPTION Sets the various registry keys to set the target version for Windows Update. This can be used to keep machines on Windows 10, instead of upgrading to Windows 11 or to target a specific "maximum" feature update version. .NOTES 2023-04-05: Fix output messages 2023-02-16: Parameterise the script to allow more control over the target versions 2022-01-25: Initial version .LINK Blog post: https://homotechsual.dev/2023/02/16/Targeting-Windows-Versions-for-Feature-Updates/ #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'RMM script - not useful to implement ShouldProcess')] param ( [Switch]$Test, [Switch]$Unset, [String]$TargetProductVersion = '22H2', [String]$TargetProduct = 'Windows 11' ) function Test-UpdateSettings { $UpdateSettings = Get-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' $Message = [System.Collections.Generic.List[String]]::New() if ($UpdateSettings.TargetReleaseVersion -and $UpdateSettings.TargetReleaseVersion -ne 0) { $Message.Add('Windows Update is currently set to target a specific release version.') if ($UpdateSettings.TargetReleaseVersionInfo) { $Message.Add("Target release version: $($UpdateSettings.TargetReleaseVersionInfo.ToString())") } else { $Message.Add('Target release version is not set.') } if ($UpdateSettings.ProductVersion) { $Message.Add("Product version: $($UpdateSettings.ProductVersion.ToString())") } else { $Message.Add('Product version is not set.') } } else { $Message.Add('Windows Update is currently set to target all versions.') } if ($String -is [array] -or $String.Count -gt 0) { return $Message.Join(' ') } else { return $Message } } function Set-UpdateSettings ([switch]$Unset) { if ($Unset) { try { Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersion' -Value 0 -Type DWord if (Test-Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\TargetReleaseVersionInfo') { Remove-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersionInfo' } if (Test-Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\ProductVersion') { Remove-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'ProductVersion' } } catch { Throw $_ } $Message = 'Windows Update is now set to target all versions.' } else { try { Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersion' -Value 1 -Type DWord Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersionInfo' -Value $TargetProductVersion Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'ProductVersion' -Value $TargetProduct $Message = ('Windows Update is now set to target {0}, {1}' -f $TargetProduct, $TargetProductVersion) } catch { Throw $_ } } return $Message } if ($Test) { $Message = Test-UpdateSettings Write-Output $Message } elseif ($Unset) { $Message = Set-UpdateSettings -Unset Write-Output $Message } else { $Message = Set-UpdateSettings Write-Output $Message } View on GitHub Parameters When you run this script you might want to pass some parameters - here's what they do: * -Test - This will test the current target version settings and show you the results. * -Unset - This will remove the target version settings. * -TargetProductVersion - Specify the target version to aim for, examples would be 21H2 or 22H2. * -TargetProduct - Specify the target product to aim for, examples would be Windows 10 or Windows 11. Tags: * Software * Windows * Updates * Registry * PowerShell DEPLOYING PRINTIX CLIENT WITH NINJAONE February 1, 2023 · One min read Mikey O'Toole This post will show you how to deploy the Printix client using NinjaOne Documentation fields and a PowerShell script. CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding two documentation fields to facilitate this script. You'll need to note your document template id, in the screenshots / our internal use we have a template called "Integration Identifiers" which we use to store any integration identifiers we need to reference in our scripts. Field LabelField NameField TypeDescriptionPrintix Tenant IdprintixTenantIdTextHolds the customer's Printix tenant id.Printix Tenant DomainprintixTenantDomainTextHolds the customer's Printix domain. THE SCRIPT Install-PrintixClient.ps1 <# .SYNOPSIS Software Deployment - NinjaOne - Printix Client .DESCRIPTION Uses documentation fields to pull client specific Printix information to download that client's installer from Printix and install it on the endpoint. .NOTES 2023-02-01: Initial version .LINK Blog post: https://homotechsual.dev/2023/02/01/Deploy-Printix-NinjaOne/ #> [Cmdletbinding()] param ( [Parameter(Mandatory = $true)] [String]$DocumentTemplate ) try { $PrintixTenantId = Ninja-Property-Docs-Get-Single $DocumentTemplate printixTenantId $PrintixTenantDomain = Ninja-Property-Docs-Get-Single $DocumentTemplate printixTenantDomain Write-Verbose ('Found Printix Tenant: {0} ({1})' -f $PrintixTenantId, $PrintixTenantDomain) if (-not ([String]::IsNullOrEmpty($PrintixTenantId) -and ([String]::IsNullOrEmpty($PrintixTenantDomain)))) { $PrintixInstallerURL = ('https://api.printix.net/v1/software/tenants/{0}/appl/CLIENT/os/WIN/type/MSI' -f $PrintixTenantId) Write-Verbose ('Built Printix Installer URL: {0}' -f $PrintixInstallerURL) $PrintixFileName = "CLIENT_{$PrintixTenantDomain}_{$PrintixTenantId}.msi" $PrintixSavePath = 'C:\RMM\Installers' if (-not (Test-Path $PrintixSavePath)) { New-Item -Path $PrintixSavePath -ItemType Directory | Out-Null } $PrintixInstallerPath = ('{0}\{1}' -f $PrintixSavePath, $PrintixFileName) Invoke-WebRequest -Uri $PrintixInstallerURL -OutFile $PrintixInstallerPath -Headers @{ 'Accept' = 'application/octet-stream' } if (Test-Path $PrintixInstallerPath) { Start-Process -FilePath 'msiexec.exe' -ArgumentList @( '/i', ('"{0}"' -f $PrintixInstallerPath), '/quiet', ('WRAPPED_ARGUMENTS=/id:{0}' -f $PrintixTenantId) ) -Wait } else { Write-Error ('Printix installer not found in {0}' -f $PrintixInstallerPath) } } } catch { Write-Error ('Failed to install Printix Client: `r`n {0}' -f $_) } View on GitHub Parameters When you run this script you need to pass your document template id. For example, sticking with our example above, you'd run the script with the parameter: -DocumentTemplate Integration Identifiers THE RESULTS We run this script on a group of devices which don't have the Printix client installed. Tags: * Software * Printix * Deployment * NinjaOne * Custom Fields * PowerShell DOWNLOADING CVE-2022-41099 PATCH AND SSU FILES January 17, 2023 · 5 min read Mikey O'Toole COLLABORATION WITH MARTIN HIMKEN This post and the WinRE patching script on Martin's blog at https://manima.de are the result of a collaboration between Martin and I to help mutually improve our various efforts towards patching CVE-2022-41099. Read Martin's blog on WinRE patching security This article relates to CVE-2022-41099 which is a vulnerability in the Windows Recovery Environment (WinRE) which could allow a successful attacker to bypass the BitLocker Device Encryption feature on the system storage device. An attacker with physical access to the target could exploit this vulnerability to gain access to encrypted data. If you're running Windows 10 or 11 you might have come across CVE-2022-41099 which is a vulnerability in the Windows Recovery Environment (WinRE) which could allow a successful attacker to bypass BitLocker if they can boot the device to WinRE. This is a pretty serious vulnerability and Microsoft have released a patch for it. However, the patch is not applied automatically and you need to take action to apply it. Martin Himken has written a script to patch the WinRE drivers and I've written a script to download and stage the patch and servicing stack update files. The link to Martin's blog is at the top of this post and will be repeated at the end. This script takes a few parameters to control it's behaviour. Parameter documentation follows: ParameterTypeDescriptionPatchFolderDirectoryInfoThe folder to download the patch files to. If not specified, C:\RMM\CVEs\2022-41099\ will be used.AllSwitchIf specified, the script will download the patch files for all supported versions and available architechtures of Windows 10 and 11. If not specified, the script will only download the patch files for the version of Windows that is running on the device. -All Using the -All parameter will download a lot of files and take a long time to complete. It is recommended that you only use this parameter if you are patching a large number of devices or want to prepare a cache to serve files from. Downloading all files consumes roughly 4.9GB of disk space. THE SCRIPT info This new version of the script downloads the Safe OS Dynamic Update (SODU) files - these are tiny and designed only to patch the vulnerable components. Safe OS Dynamic Update (SODU) version. 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/03/22. Get-CVE202241099Patches.ps1 <# .SYNOPSIS Update Management - CVE-2022-41099 Downloader (Safe OS Dynamic Update version) .DESCRIPTION This script will download the applicable patch for CVE-2022-41099 from Microsoft Update Catalog. It will detect the OS version and bitness and download the correct patch. This uses the Feb 2023, January 2023 or November 2022 Safe OS DU depending on the OS version. .PARAMETER PatchFolder Accepts the path to a folder to download the patch(es) to. .EXAMPLE This example will download the applicable patch for CVE-2022-41099 to the folder C:\RMM\CVEs\2022-41099\. Get-CVE202241099Patches.ps1 -PatchFolder 'C:\RMM\CVEs\2022-41099\' .EXAMPLE This example will download all patches for CVE-2022-41099 to the folder C:\RMM\CVEs\2022-41099\. With subfolders for each KB and architecture. Get-CVE202241099Patches.ps1 -PatchFolder 'C:\RMM\CVEs\2022-41099\' -All .NOTES 2023-03-22: Empty the patch folder if it's not empty. Thanks to Wisecompany for the suggestion. 2023-03-22: Fixes incorrectly switched URLs for 19042 to 19045 for the x86 and x64 downloads. Thanks to Wisecompany for helping find this. 2023-03-21: Update to use the Safe OS Dynamic Update packages which are considerably smaller. 2023-01-18: Use `$ProgressPreference` to speed up execution. Thanks to CodyRWhite (GitHub) for the suggestion. 2023-01-18: Fix bug accessing hashtable of architectures using Windows build number. Thanks to Sir Loin (WinAdmins) for spotting this. 2023-01-17: Add support for Windows 10 19044 (21H2) 2023-01-17: Initial version .LINK Blog post: https://homotechsual.dev/2023/01/17/Download-CVE-2022-41099-Patches/ #> # We're targetting the January 2023 Safe OS Dynamic Update for Windows 11 22H2 and the November 2022 Safe OS Dynamic Update for Windows 11 21H2 and Windows 10 22H2, 22H1, 21H1 and 20H2. [CmdletBinding()] param ( # The path to the folder to download the patch(es) to. [System.IO.DirectoryInfo]$PatchFolder = 'C:\RMM\CVEs\2022-41099\', # Download all patches including SSUs useful if you want to populate a staging folder. Will create a subfolder for each. [Switch]$All ) $OriginalProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $BuildtoKBMap = @{ 22623 = 5023527 22621 = 5023527 22000 = 5021040 19045 = 5021043 19044 = 5021043 19043 = 5021043 19042 = 5021043 } $KBtoCABMap = @{ # KB5023527 - February 2023 5023527 = @{ 'x64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/crup/2023/02/windows11.0-kb5023527-x64_076cd9782ebb8aed56ad5d99c07201035d92e66a.cab' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/crup/2023/02/windows11.0-kb5023527-arm64_bd0a8952aee4f003c26e272a9b804645146e9358.cab' } # KB5021040 - November 2022 5021040 = @{ 'x64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/crup/2022/11/windows10.0-kb5021040-x64_2216fe185502d04d7a420a115a53613db35c0af9.cab' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/crup/2022/11/windows10.0-kb5021040-arm64_cb0622a2c0ef781826f583eccd16c289597678cc.cab' } # KB5021043 - January 2023 5021043 = @{ 'x86' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/crup/2022/11/windows10.0-kb5021043-x86_484ed491379e442debef6fdfb6860be749145017.cab' 'x64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/crup/2022/11/windows10.0-kb5021043-x64_efa19d2d431c5e782a59daaf2d04d026bb8c8e76.cab' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/crup/2022/11/windows10.0-kb5021043-arm64_59eed783be1d2a514c01cb325cfad83ea65f7515.cab' } } $WinOSBuild = [System.Environment]::OSVersion.Version.Build $WinOSArch = if ([System.Environment]::Is64BitOperatingSystem) { 'x64' } else { 'x86' } if (-not (Test-Path -Path $PatchFolder)) { New-Item -Path $PatchFolder -ItemType Directory | Out-Null } else { if (Get-ChildItem -Path $PatchFolder -Recurse) { Write-Verbose "Emptying $PatchFolder" Get-ChildItem -Path $PatchFolder -Recurse | Remove-Item -Force -Recurse } } if (-not $All) { try { if (-not $BuildtoKBMap.ContainsKey($WinOSBuild)) { Write-Error "Unsupported Windows version $WinOSBuild" exit 1 } else { $KB = $BuildtoKBMap[$WinOSBuild] if (-not $KBtoCABMap.ContainsKey($KB)) { Write-Error "Did not find patches for KB $KB" exit 1 } else { $DownloadUrl = $KBtoCABMap[$KB][$WinOSArch] $FileName = ([URI]$DownloadUrl).Segments[-1] $TargetPath = Join-Path -Path $PatchFolder -ChildPath $FileName if (-not (Test-Path -Path $TargetPath)) { Write-Verbose "Downloading $FileName" Invoke-WebRequest -Uri $DownloadUrl -OutFile $TargetPath } } } } catch [System.Net.WebException] { Write-Error "Failed to download one or more CAB files for $WinOSBuild $WinOSArch" Write-Error $_.Exception.Message exit 1 } catch [System.IO.IOException] { Write-Error "Could not write to $PatchFolder" Write-Error $_.Exception.Message } } else { try { $KBtoCABMap.GetEnumerator() | ForEach-Object { $PatchSubFolder = Join-Path -Path $PatchFolder -ChildPath $_.Name Write-Warning "Downloading Patch CAB files to: $PatchSubFolder." if (-not (Test-Path -Path $PatchSubFolder)) { New-Item -Path $PatchSubFolder -ItemType Directory | Out-Null } foreach ($Arch in $_.Value.GetEnumerator()) { $ArchSubFolder = Join-Path -Path $PatchSubFolder -ChildPath $Arch.Name if (-not (Test-Path -Path $ArchSubFolder)) { New-Item -Path $ArchSubFolder -ItemType Directory | Out-Null } $DownloadUrl = $Arch.Value $FileName = ([URI]$DownloadUrl).Segments[-1] $TargetPath = Join-Path -Path $ArchSubFolder -ChildPath $FileName if (-not (Test-Path -Path $TargetPath)) { Write-Verbose "Downloading $FileName" Invoke-WebRequest -Uri $DownloadUrl -OutFile $TargetPath } else { Write-Verbose "Skipping $FileName as it already exists" } } } } catch [System.Net.WebException] { Write-Error "Failed to download one or more CAB files for $WinOSBuild $WinOSArch" Write-Error $_.Exception.Message exit 1 } catch [System.IO.IOException] { Write-Error "Could not write to $PatchFolder" Write-Error $_.Exception.Message } } $ProgressPreference = $OriginalProgressPreference View on GitHub Change Logs VERSION: 1.5 Fixes incorrectly switched URLs for 19042 to 19045 for the x86 and x64 downloads. Thanks to Wisecompany for helping find this. VERSION: 1.4 Update to use the Safe OS Dynamic Update packages which are considerably smaller. VERSION: 1.3 Use $ProgressPreference to speed up execution. Thanks to https://github.com/CodyRWhite for the suggestion. VERSION: 1.2 Fix a bug on line 82 where a hashtable of architectures was attempted to be accessed using the Windows build number. Thanks to Sir Loin of House WinAdmins for spotting this. (Yes, it's a Game of Thrones reference. So original.) VERSION: 1.1 Adds handling for 19044. VERSION: 1.0 Initial release. info This version of the script downloads the SSU and Dynamic Cumulative Update files - these are large and designed to update WinRE completely not just patch the vulnerability. Servicing Stack Update (SSU) and Dynamic Cumulative Update (DCU) version. Large Files This script downloads the SSU and Dynamic Cumulative Update files - these are large and designed to update WinRE completely not just patch the vulnerability. This will require a lot of space both to download them (especially if using -All) and to apply them to WinRE. 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/03/22. Get-CVE202241099Patches.ps1 <# .SYNOPSIS Update Management - CVE-2022-41099 Downloader .DESCRIPTION This script will download the applicable patch for CVE-2022-41099 from Microsoft Update Catalog. It will detect the OS version and bitness and download the correct patch. This uses the January 2023 Servicing Stack Update (SSU) as the ultimate target. .NOTES 2023-03-22: Empty the patch folder if it's not empty. Thanks to Wisecompany for the suggestion. 2023-01-18: Use `$ProgressPreference` to speed up execution. Thanks to CodyRWhite (GitHub) for the suggestion. 2023-01-18: Fix bug accessing hashtable of architectures using Windows build number. Thanks to Sir Loin (WinAdmins) for spotting this. 2023-01-17: Add support for Windows 10 19044 (21H2) 2023-01-17: Initial version .LINK Blog post: https://homotechsual.dev/2023/01/17/Download-CVE-2022-41099-Patches/ .EXAMPLE This example will download the applicable patch for CVE-2022-41099 to the folder C:\RMM\CVEs\2022-41099\. Get-CVE202241099Patches.ps1 -PatchFolder 'C:\RMM\CVEs\2022-41099\' .EXAMPLE This example will download all patches for CVE-2022-41099 to the folder C:\RMM\CVEs\2022-41099\. With subfolders for each KB and architecture. Get-CVE202241099Patches.ps1 -PatchFolder 'C:\RMM\CVEs\2022-41099\' -All #> # We're targetting the January 2023 CU as our version to patch to. The CVE page links to the November 2022 CU, but the January 2023 CU is the latest version and refers again to the vulnerability so it seems safer to patch to that version. ARM links are included but the script does not handle ARM detection yet. We also download, where applicable the latest SSU. [CmdletBinding()] param ( # The path to the folder to download the patch(es) to. [System.IO.DirectoryInfo]$PatchFolder = 'C:\RMM\CVEs\2022-41099\', # Download all patches including SSUs useful if you want to populate a staging folder. Will create a subfolder for each. [Switch]$All ) $OriginalProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $BuildtoKBMap = @{ 22623 = 5022303 22621 = 5022303 22000 = 5022287 19045 = 5022282 19044 = 5022282 19043 = 5022282 19042 = 5021233 } $KBtoMSUMap = @{ # KB5022303 - January 2023 5022303 = @{ 'x64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2023/01/windows11.0-kb5022303-x64_87d49704f3f7312cddfe27e45ba493048fdd1517.msu' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2023/01/windows11.0-kb5022303-arm64_4c207b992ed272bbdbfb35d77f0458548a7b86d1.msu' } # KB5022287 - January 2023 5022287 = @{ 'x64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2023/01/windows10.0-kb5022287-x64_55641f1989bae2c2d0f540504fb07400a0f187b3.msu' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2023/01/windows10.0-kb5022287-arm64_7d26e9ef2c00ce384e19d4ca234052e378747a05.msu' } # KB5022282 - January 2023 5022282 = @{ 'x86' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2023/01/windows10.0-kb5022282-x86_5fb142aca9e3f8c7ed37df9e7806b7f7f56d9599.msu' 'x64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2023/01/windows10.0-kb5022282-x64_fdb2ea85e921869f0abe1750ac7cee34876a760c.msu' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2023/01/windows10.0-kb5022282-arm64_9ccaddc4356ab1db614881e08635bd8959ff97f3.msu' } # KB5021233 - December 2022 5021233 = @{ 'x86' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2022/12/windows10.0-kb5021233-x86_e21531f654715af20b2aa329d6786080bd798963.msu' 'x64' = 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2022/12/windows10.0-kb5021233-x64_00bbf75a829a2cb4f37e4a2b876ea9503acfaf4d.msu' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2022/12/windows10.0-kb5021233-arm64_4bac0de318c939e54fa6a9f537e892272446ae09.msu' } } $ArchtoSSUMap = @{ 'x86' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2022/05/ssu-19041.1704-x86_3cec66c3891a613e6656f141547e573f9d700d35.msu' 'x64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2022/05/ssu-19041.1704-x64_70e350118b85fdae082ab7fde8165a947341ba1a.msu' 'ARM64' = 'https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2022/05/ssu-19041.1704-arm64_dac34c98382f951bd654fe3affe0b3e7100b3745.msu' } $SSUKB = '5013942' $WinOSBuild = [System.Environment]::OSVersion.Version.Build $WinOSArch = if ([System.Environment]::Is64BitOperatingSystem) { 'x64' } else { 'x86' } if (-not (Test-Path -Path $PatchFolder)) { New-Item -Path $PatchFolder -ItemType Directory | Out-Null } else { if (Get-ChildItem -Path $PatchFolder -Recurse) { Write-Verbose "Emptying $PatchFolder" Get-ChildItem -Path $PatchFolder -Recurse | Remove-Item -Force -Recurse } } if (-not $All) { try { if ($WinOSBuild -lt 22000 -and $WinOSBuild -ge 19042) { if ($ArchtoSSUMap.ContainsKey($WinOSArch)) { $DownloadUrl = $ArchtoSSUMap[$WinOSArch] $FileName = ([URI]$DownloadUrl).Segments[-1] $TargetPath = Join-Path -Path $PatchFolder -ChildPath ("1_$FileName") if (-not (Test-Path -Path $TargetPath)) { Write-Verbose "Downloading $FileName" Invoke-WebRequest -Uri $DownloadUrl -OutFile $TargetPath } } } if (-not $BuildtoKBMap.ContainsKey($WinOSBuild)) { Write-Error "Unsupported Windows version $WinOSBuild" exit 1 } else { $KB = $BuildtoKBMap[$WinOSBuild] if (-not $KBtoMSUMap.ContainsKey($KB)) { Write-Error "Did not find patches for KB $KB" exit 1 } else { $DownloadUrl = $KBtoMSUMap[$KB][$WinOSArch] $FileName = ([URI]$DownloadUrl).Segments[-1] $TargetPath = Join-Path -Path $PatchFolder -ChildPath $FileName if (-not (Test-Path -Path $TargetPath)) { Write-Verbose "Downloading $FileName" Invoke-WebRequest -Uri $DownloadUrl -OutFile $TargetPath } } } } catch [System.Net.WebException] { Write-Error "Failed to download one or more MSU files for $WinOSBuild $WinOSArch" Write-Error $_.Exception.Message exit 1 } catch [System.IO.IOException] { Write-Error "Could not write to $PatchFolder" Write-Error $_.Exception.Message } } else { try { $SSUSubFolder = Join-Path -Path $PatchFolder -ChildPath $SSUKB Write-Warning "Downloading SSU MSU files to: $SSUSubFolder." if (-not (Test-Path -Path $SSUSubFolder)) { New-Item -Path $SSUSubFolder -ItemType Directory | Out-Null } $ArchtoSSUMap.GetEnumerator() | ForEach-Object { $ArchSubFolder = Join-Path -Path $SSUSubFolder -ChildPath $_.Name if (-not (Test-Path -Path $ArchSubFolder)) { New-Item -Path $ArchSubFolder -ItemType Directory | Out-Null } $DownloadUrl = $_.Value $FileName = ([URI]$DownloadUrl).Segments[-1] $TargetPath = Join-Path -Path $ArchSubFolder -ChildPath $FileName if (-not (Test-Path -Path $TargetPath)) { Write-Verbose "Downloading $FileName" Invoke-WebRequest -Uri $DownloadUrl -OutFile $TargetPath } else { Write-Verbose "Skipping $FileName as it already exists" } } $KBtoMSUMap.GetEnumerator() | ForEach-Object { $PatchSubFolder = Join-Path -Path $PatchFolder -ChildPath $_.Name Write-Warning "Downloading Patch MSU files to: $PatchSubFolder." if (-not (Test-Path -Path $PatchSubFolder)) { New-Item -Path $PatchSubFolder -ItemType Directory | Out-Null } foreach ($Arch in $_.Value.GetEnumerator()) { $ArchSubFolder = Join-Path -Path $PatchSubFolder -ChildPath $Arch.Name if (-not (Test-Path -Path $ArchSubFolder)) { New-Item -Path $ArchSubFolder -ItemType Directory | Out-Null } $DownloadUrl = $Arch.Value $FileName = ([URI]$DownloadUrl).Segments[-1] $TargetPath = Join-Path -Path $ArchSubFolder -ChildPath $FileName if (-not (Test-Path -Path $TargetPath)) { Write-Verbose "Downloading $FileName" Invoke-WebRequest -Uri $DownloadUrl -OutFile $TargetPath } else { Write-Verbose "Skipping $FileName as it already exists" } } } } catch [System.Net.WebException] { Write-Error "Failed to download one or more MSU files for $WinOSBuild $WinOSArch" Write-Error $_.Exception.Message exit 1 } catch [System.IO.IOException] { Write-Error "Could not write to $PatchFolder" Write-Error $_.Exception.Message } } $ProgressPreference = $OriginalProgressPreference View on GitHub Change Logs VERSION: 1.4 Empty the patch folder if it's not empty. Thanks to Wisecompany for the suggestion. VERSION: 1.3 Use $ProgressPreference to speed up execution. Thanks to https://github.com/CodyRWhite for the suggestion. VERSION: 1.2 Fix a bug on line 82 where a hashtable of architectures was attempted to be accessed using the Windows build number. Thanks to Sir Loin of House WinAdmins for spotting this. (Yes, it's a Game of Thrones reference. So original.) VERSION: 1.1 Adds handling for 19044. VERSION: 1.0 Initial release. EXAMPLES This example will download the applicable patch and SSU (if applicable) for CVE-2022-41099 to the folder C:\RMM\CVEs\2022-41099. Get-CVE202241099Patches.ps1 -PatchFolder 'C:\RMM\CVEs\2022-41099\' This example will download all patches for CVE-2022-41099 to the folder C:\RMM\CVEs\2022-41099. With subfolders for each KB and architecture. Get-CVE202241099Patches.ps1 -PatchFolder 'C:\RMM\CVEs\2022-41099\' -All VALIDATING THE FIX By popular request you can validate the fix using the principles in the script used for CVE detection. COLLABORATION WITH MARTIN HIMKEN This post and the WinRE patching script on Martin's blog at https://manima.de are the result of a collaboration between Martin and I to help mutually improve our various efforts towards patching CVE-2022-41099. Read Martin's blog on WinRE patching Tags: * Collaboration * Vulnerability * Security * Windows * CVE * PowerShell SENDING TOAST NOTIFICATIONS IN WINDOWS 10 AND 11 January 17, 2023 · 8 min read Mikey O'Toole THIS POST USES CODE IN PART FROM THE SMSAGENT BLOG. This post takes a snippet from the SMSAgent Blog and adds some additional magic along with two new custom functions. View the original post If you're a Windows 10 or 11 user you'll be familiar with the toast notifications that appear in the bottom right of your screen. These are a great way to get a quick message to the user without interrupting what they're doing. In this article we'll look at how to send a toast notification from PowerShell. We could use the excellent BurntToast PowerShell module to send a toast notification, but in the interests of reducing the number of third-party modules installed on client machines we'll be using the underlying .NET APIs directly as our needs are fairly simple. Sending toast notifications is fairly simple once you get to grips with the underlying XML schema but we want our Toasts to be next-level so we're going to make them CREATING A NOTIFICATION APP This script takes a few parameters to create a Notification App. The App Name and Icon are the most important as these are what will appear in the toast notification. Parameter documentation follows: ParameterTypeDescriptionIconURIURIThe URI of the app icon to use for the notification app registration. This will be downloaded to the working directory.IconFileNameStringFile name to use for the app icon. Optional. If not specified, the file name from the URI will be used.WorkingDirectoryDirectoryInfoThe working directory to use for the app icon. If not specified, 'C:\RMM\NotificationApp' will be used.AppIdStringThe app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.AppDisplayNameStringThe app display name to use for the notification app registration.AppIconBackgroundColorStringThe background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent.ShowInSettingsIntShow the app in the Windows Settings app. Optional. If not specified, the app will not be shown in the Settings app. THE SCRIPT New-NotificationApp.ps1 <# .SYNOPSIS Utilities - Windows - Notifications - Register Notification App .DESCRIPTION Registers a notification app into the Windows registry allowing toast notifications to be sent using the App's Id which provides for control over the application display name and the app icon. .NOTES 2023-01-17: Initial version .LINK Blog post: https://homotechsual.dev/2023/01/17/Toast-Notifications-Windows-10-and-11/ #> #requires -RunAsAdministrator [CmdletBinding()] Param( # The URI of the app icon to use for the notification app registration. [Parameter(Mandatory)] [uri]$IconURI, # File name to use for the app icon. Optional. If not specified, the file name from the URI will be used. [string]$IconFileName, # The working directory to use for the app icon. If not specified, 'C:\RMM\NotificationApp\' will be used. [System.IO.DirectoryInfo]$WorkingDirectory = 'C:\RMM\NotificationApp\', # The app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'. [Parameter(Mandatory)] [string]$AppId, # The app display name to use for the notification app registration. [Parameter(Mandatory)] [string]$AppDisplayName, # The background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent. [ValidatePattern('^(0)$|^([A-F0-9]{8})$')] [string]$AppIconBackgroundColor = 0, # Whether or not to show the app in the Windows Settings app. Optional. If not specified, the app will not be shown in the Settings app. Expected values are 0 or 1 (0 = false, 1 = true). [int]$ShowInSettings = 0 ) # Functions function Get-NotificationApp { <# .SYNOPSIS Gets the notification app registration information. #> [CmdletBinding()] Param( [Parameter(Mandatory)] [string]$AppId ) $HKCR = Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue If (!($HKCR)) { $null = New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Script } $AppRegPath = 'HKCR:\AppUserModelId' $RegPath = "$AppRegPath\$AppID" If (!(Test-Path $RegPath)) { return $null } else { $NotificationApp = Get-Item -Path $RegPath return $NotificationApp } } Function Register-NotificationApp { <# .SYNOPSIS Registers an application to receive toast notifications. .NOTES Original Author: Trevor Jones Original Author Link: https://smsagent.blog/author/trevandju/ Version: 2.0 Version Date: 2023-01-17 Version Description: Added AppIcon and AppIconBackground parameters. Version Author: Mikey O'Toole Version: 1.0 Version Date: 2020-10-20 Version Description: Initial release by Trevor Jones. .LINK https://smsagent.blog/2020/10/20/adding-your-own-caller-app-for-custom-windows-10-toast-notifications/ #> [CmdletBinding()] Param( [Parameter(Mandatory)] [string]$AppId, [Parameter(Mandatory)] [string]$AppDisplayName, [System.IO.FileInfo]$AppIcon = $null, [ValidatePattern('^(0)$|^([A-F0-9]{8})$')] [string]$AppIconBackgroundColor = $null, [int]$ShowInSettings = 0 ) $HKCR = Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue If (!($HKCR)) { $null = New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Script } $AppRegPath = 'HKCR:\AppUserModelId' $RegPath = "$AppRegPath\$AppId" If (!(Test-Path $RegPath)) { $null = New-Item -Path $AppRegPath -Name $AppId -Force } $DisplayName = Get-ItemProperty -Path $RegPath -Name DisplayName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DisplayName -ErrorAction SilentlyContinue If ($DisplayName -ne $AppDisplayName) { $null = New-ItemProperty -Path $RegPath -Name DisplayName -Value $AppDisplayName -PropertyType String -Force } $Icon = Get-ItemProperty -Path $RegPath -Name IconUri -ErrorAction SilentlyContinue | Select-Object -ExpandProperty IconUri -ErrorAction SilentlyContinue if ($Icon -ne $AppIcon) { $null = New-ItemProperty -Path $RegPath -Name IconUri -Value $AppIcon -PropertyType String -Force } $BackgroundColor = Get-ItemProperty -Path $RegPath -Name IconBackgroundColor -ErrorAction SilentlyContinue | Select-Object -ExpandProperty IconBackgroundColor -ErrorAction SilentlyContinue if ($BackgroundColor -ne $AppIconBackgroundColor) { $null = New-ItemProperty -Path $RegPath -Name IconBackgroundColor -Value $AppIconBackgroundColor -PropertyType String -Force } $ShowInSettingsValue = Get-ItemProperty -Path $RegPath -Name ShowInSettings -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ShowInSettings -ErrorAction SilentlyContinue If ($ShowInSettingsValue -ne $ShowInSettings) { $null = New-ItemProperty -Path $RegPath -Name ShowInSettings -Value $ShowInSettings -PropertyType DWORD -Force } $null = Remove-PSDrive -Name HKCR -Force } function Get-AppIcon { <# .SYNOPSIS Downloads the app icon from a URI and saves it to a file. #> [CmdletBinding()] Param( [Parameter(Mandatory)] [uri]$IconURI, [string]$IconFileName = $null, [Parameter(Mandatory)] [System.IO.DirectoryInfo]$WorkingDirectory ) if (!($WorkingDirectory.Exists)) { $WorkingDirectory.Create() } if (!($IconFileName)) { $IconFileName = $IconURI.Segments[-1] } $IconFilePath = Join-Path -Path $WorkingDirectory.FullName -ChildPath $IconFileName $IconFile = New-Object System.IO.FileInfo $IconFilePath If ($IconFile.Exists) { $IconFile.Delete() } Invoke-WebRequest -Uri $IconURI -OutFile $IconFile.FullName | Out-Null return $IconFile.FullName } # Main Script $AppId = $AppId.TrimStart('"').TrimEnd('"') $AppIcon = Get-AppIcon -IconURI $IconURI -WorkingDirectory $WorkingDirectory $NotificationAppParams = @{ AppID = $AppId AppDisplayName = $AppDisplayName AppIcon = $AppIcon AppIconBackgroundColor = $AppIconBackgroundColor } if ($ShowInSettings) { $NotificationAppParams.Add('ShowInSettings', $ShowInSettings) } Register-NotificationApp @NotificationAppParams $NotificationApp = Get-NotificationApp -AppID $AppId if (!($NotificationApp)) { Write-Error 'Failed to register the notification app.' Exit 1 } else { Write-Output ('Successfully registered the notification app {0}.' -f $NotificationApp.GetValue('DisplayName')) Exit 0 } View on GitHub GENERIC EXAMPLE # Setup parameter hashtable. $NotificationAppParams = @{ IconURI = 'https://homotechsual.dev/img/Icon.png' IconFileName = 'homotechsual.png' WorkingDirectory = 'C:\RMM\NotificationApp' AppId = 'homotechsual.example' AppDisplayName = 'Homotechsual Example' AppIconBackgroundColor = 0 ShowInSettings = 1 } Register-NotificationApp @NotificationAppParams RMM EXAMPLE Unsurprisingly, I'm going to use NinjaOne as the example RMM here. So I've done the donkeywork that is best documented by NinjaOne themselves on the Dojo and added the script above to the NinjaOne Script Library - but what next? OPTION 1: SAME APP FOR ALL CUSTOMERS Well the chances are that you want to use the same relatively small set of apps per customer so we'll just store a nice blob of text in the Ninja Script Library. We could end up with a "Preset Parameter" set like this: -IconURI https://homotechsual.dev/img/Icon.png -IconFileName homotechsual.png -WorkingDirectory C:\RMM\NotificationApp -AppId homotechsual.example -AppDisplayName "Homotechsual Example" -AppIconBackgroundColor 0 -ShowInSettings 1 You can have multiple preset parameter sets and simply select the one you want to use when you run the script or when you schedule it against a group (or however you want to run this) but what about getting more creative? OPTION 2: PULL APP DETAILS PER CUSTOMER We can use Documentation fields to store the details of the app per client and then use the NinjaOne CLI to pull the details when we run the script. This is a little more work but it's worth it if you want to brand your notifications per customer. We'll need to add a few fields to the NinjaOne Documentation tab for each client: FieldTypeDescriptionNotificationIconURIURLThe URI of the app icon to use for the notification app registration. This will be downloaded to the working directory.NotificationAppIdStringThe app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.NotificationAppDisplayNameStringThe app display name to use for the notification app registration.NotificationAppBackgroundColorStringThe background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent. We can then use the NinjaOne CLI to pull the details for the client we're running the script against and use them to create the parameter set for the script. This means editing our script a little to accept the parameters from the CLI and then using the NinjaOne CLI to pull the details from the Documentation tab. We're going to assume you called your Document Template Example Template and your fields named as above in the table (with spaces between words if you wish). At the top of the script we're going to replace the entire parameter block: * Before * After [CmdletBinding()] Param( # The URI of the app icon to use for the notification app registration. [Parameter(Mandatory)] [uri]$IconURI, # File name to use for the app icon. Optional. If not specified, the file name from the URI will be used. [string]$IconFileName, # The working directory to use for the app icon. If not specified, 'C:\RMM\NotificationApp\' will be used. [System.IO.DirectoryInfo]$WorkingDirectory = 'C:\RMM\NotificationApp\', # The app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'. [Parameter(Mandatory)] [string]$AppId, # The app display name to use for the notification app registration. [Parameter(Mandatory)] [string]$AppDisplayName, # The background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent. [ValidatePattern('^(0)$|^([A-F0-9]{8})$')] [string]$AppIconBackgroundColor = 0, # Whether or not to show the app in the Windows Settings app. Optional. If not specified, the app will not be shown in the Settings app. Expected values are 0 or 1 (0 = false, 1 = true). [int]$ShowInSettings = 0 ) And replace it with this: $IconURI = Ninja-Docs-Property-Get "Example Template" "NotificationIconURI" $IconFileName = 'NotificationIcon.png' $WorkingDirectory = 'C:\RMM\NotificationApp\' $AppId = Ninja-Docs-Property-Get "Example Template" "NotificationAppId" $AppDisplayName = Ninja-Docs-Property-Get "Example Template" "NotificationAppDisplayName" $AppIconBackgroundColor = Ninja-Docs-Property-Get "Example Template" "NotificationAppBackgroundColor" $ShowInSettings = 1 If done correctly, you should be able to run the script and have it pull the details from the Documentation tab for the client you're running it against. This could be improved by exiting the script if the required fields are not populated so we could add something like this after $ShowInSettings = 1: if (($null -eq $IconURI) -or ($null -eq $AppId) -or ($null -eq $AppDisplayName)) { Write-Error "Required fields are not populated in the Documentation tab." exit } Well that's setting up the notification app registration. Now we need to actually send a notification using the app. SENDING A NOTIFICATION So this is an entirely separate script - you can use it with the custom app you just created or you can use it with the default where notifications come from Windows PowerShell. It's up to you. "Runs as User" This script runs in the user's context it is unlikely it'll work running as Administrator or as a scheduled task. If you want to run it as a scheduled task, you'll need to use something like RunAsUser to run it as the user. This script takes a few parameters to create a Notification App. The App Name and Icon are the most important as these are what will appear in the toast notification. Parameter documentation follows: ParameterTypeDescriptionAppIdStringThe app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.NotificationImageStringThe path to the image to use for the notification.NotificationTitleStringThe title to use for the notification.NotificationMessageStringThe message text to use for the notification.NotificationTypeStringThe type of notification to send. Expected values are alarm, reminder, incomingcall or default. Details on what each type looks like can be found in Microsoft's Toast schema. THE SCRIPT Send-SimpleNotification.ps1 #requires -version 5.1 <# .SYNOPSIS Utilities - Windows - Notifications - Send Notification (Simple) .DESCRIPTION Sends a simple toast notification to the Windows notification center. .NOTES 2023-01-17: Initial version .LINK Blog post: https://homotechsual.dev/2023/01/17/Toast-Notifications-Windows-10-and-11/ #> [CmdletBinding()] Param( [string]$AppID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe', [string]$NotificationImage, [Parameter(Mandatory)] [string]$NotificationTitle, [Parameter(Mandatory)] [string]$NotificationMessage, [ValidateSet('alarm', 'reminder', 'incomingcall', 'default')] [string]$NotificationType = 'default' ) $null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] $null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] $NotificationTemplate = [xml]@" <toast scenario="$NotificationType"> <visual> <binding template="ToastGeneric"> <text>$NotificationTitle</text> <text>$NotificationMessage</text> <image placement="appLogoOverride" src="$NotificationImage"/> </binding> </visual> </toast> "@ $NotificationXML = [Windows.Data.XML.DOM.XMLDocument]::New() $NotificationXML.LoadXml($NotificationTemplate.OuterXml) $Toast = [Windows.UI.Notifications.ToastNotification]::new($NotificationXML) $ToastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppID) $ToastNotifier.Show($Toast) View on GitHub GENERIC EXAMPLE # Setup parameter hashtable. $NotificationParams = @{ AppId = 'homotechsual.example' NotificationImage = 'C:\RMM\NotificationApp\NotificationIcon.png' NotificationTitle = 'Example Notification' NotificationMessage = 'This is an example notification.' NotificationType = 'reminder' } Send-Notification @NotificationParams SCRIPT EXAMPLE It's probably most useful to use this script as part of a larger script. For example, you could use it to send a notification when a script has finished running. You could also use it to send a notification when a scheduled task has finished running. NotificationExample.ps1 <# .SYNOPSIS Utilities - Windows - Notifications - Example Usage .DESCRIPTION Demonstrates usage of the Send-Notification function within a larger script using a pre-registered app. .NOTES 2023-01-17: Initial version .LINK Blog post: https://homotechsual.dev/2023/01/17/Toast-Notifications-Windows-10-and-11/ #> # Functions function Send-Notification { [CmdletBinding()] Param( [string]$AppID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe', [string]$NotificationImage, [Parameter(Mandatory)] [string]$NotificationTitle, [Parameter(Mandatory)] [string]$NotificationMessage, [ValidateSet('alarm', 'reminder', 'incomingcall', 'default')] [string]$NotificationType = 'default' ) $null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] $null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] $NotificationTemplate = [xml]@" <toast scenario="$NotificationType"> <visual> <binding template="ToastGeneric"> <text>$NotificationTitle</text> <text>$NotificationMessage</text> <image placement="appLogoOverride" src="$NotificationImage"/> </binding> </visual> </toast> "@ $NotificationXML = [Windows.Data.XML.DOM.XMLDocument]::New() $NotificationXML.LoadXml($NotificationTemplate.OuterXml) $Toast = [Windows.UI.Notifications.ToastNotification]::new($NotificationXML) $ToastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppID) $ToastNotifier.Show($Toast) } # Main Loop $SpoolerService = Get-Service -Name 'Spooler' if ($SpoolerService.Status -eq 'Running') { try { Restart-Service -Name 'Spooler' $NotificationParams = @{ AppId = 'homotechsual.example' NotificationImage = 'C:\RMM\NotificationApp\NotificationIcon.png' NotificationTitle = 'Printer Spooler Restarted' NotificationMessage = 'The printer spooler service was restarted.' NotificationType = 'reminder' } Send-Notification @NotificationParams } catch { $NotificationParams = @{ AppId = 'homotechsual.example' NotificationImage = 'C:\RMM\NotificationApp\NotificationIcon.png' NotificationTitle = 'Printer Spooler Failed to Restart' NotificationMessage = 'The printer spooler service failed to restart.' NotificationType = 'reminder' } Send-Notification @NotificationParams } } View on GitHub This would output something like this: There are many other ways you could use notifications and you can add (and handle) actions within notifications but I'm spoiling some killer upcoming blog posts. Tags: * Notifications * User Experience * Windows * Client Management * PowerShell UPDATING DRIVERS FROM MICROSOFT UPDATE January 10, 2023 · One min read Mikey O'Toole CREATING FIELDS Creating custom fields in NinjaOne To create a custom field 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 and/or to a location and/or organisation Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles (for role custom fields). When you create your custom field you need to make sure that you set the Scripts permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using. We're adding three role custom fields for devices with the Windows Laptop role: Field NameField TypeDescriptionDriver Update: Reboot RequiredCheckboxWhether the latest driver update run requires a reboot to finalise.Driver Update: Last RunDate/TimeThe date and time the driver update script last ran successfully.Driver Update: Number Installed on Last RunIntegerThe number of driver updates installed on last script run. THE SCRIPT Invoke-MUDriverUpdater.ps1 <# .SYNOPSIS Update Management - Microsoft Update Driver Updater .DESCRIPTION Downloads and installs the latest drivers using Microsoft Update. .NOTES 2023-01-10: Initial version .LINK Blog post: https://homotechsual.dev/2023/01/10/Updating-Drivers-from-Microsoft-Update/ #> [CmdletBinding()] param () try { # Create a new update service manager COM object. $UpdateService = New-Object -ComObject Microsoft.Update.ServiceManager # If the Microsoft Update service is not enabled, enable it. $MicrosoftUpdateService = $UpdateService.Services | Where-Object { $_.ServiceId -eq '7971f918-a847-4430-9279-4a52d1efe18d' } if (!$MicrosoftUpdateService) { $UpdateService.AddService2('7971f918-a847-4430-9279-4a52d1efe18d', 7, '') } # Create a new update session COM object. $UpdateSession = New-Object -ComObject Microsoft.Update.Session # Create a new update searcher in the update session. $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() # Configure the update searcher to search for driver updates from Microsoft Update. ## Set the update searcher $UpdateSearcher.ServiceID = '7971f918-a847-4430-9279-4a52d1efe18d' ## Set the update searcher to search for per-machine updates only. $UpdateSearcher.SearchScope = 1 ## Set the update searcher to search non-Microsoft sources only (no WSUS, no Windows Update) so Microsoft Update and Manufacturers only. $UpdateSearcher.ServerSelection = 3 # Set our search criteria to only search for driver updates. $SearchCriteria = "IsInstalled=0 and Type='Driver'" # Search for driver updates. Write-Verbose 'Searching for driver updates...' $UpdateSearchResult = $UpdateSearcher.Search($SearchCriteria) $UpdatesAvailable = $UpdateSearchResult.Updates # If no updates are available, output a message and exit. if (($UpdatesAvailable.Count -eq 0) -or ([string]::IsNullOrEmpty($UpdatesAvailable))) { Write-Warning 'No driver updates are available.' Ninja-Property-Set driverUpdateRebootRequired 0 # Adjust for RMM Ninja-Property-Set driverUpdateLastRun (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss') # Adjust for RMM Ninja-Property-Set driverUpdateNumberInstalledOnLastRun 0 # Adjust for RMM exit 0 } else { Write-Verbose "Found $($UpdatesAvailable.Count) driver updates." # Output available updates. $UpdatesAvailable | Select-Object -Property Title, DriverModel, DriverVerDate, DriverClass, DriverManufacturer | Format-Table # Create a new update collection to hold the updates we want to download. $UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl $UpdatesAvailable | ForEach-Object { # Add the update to the update collection. $UpdatesToDownload.Add($_) | Out-Null } # If there are updates to download, download them. if (($UpdatesToDownload.count -gt 0) -or (![string]::IsNullOrEmpty($UpdatesToDownload))) { # Create a fresh session to download and install updates. $UpdaterSession = New-Object -ComObject Microsoft.Update.Session $UpdateDownloader = $UpdaterSession.CreateUpdateDownloader() # Add the updates to the downloader. $UpdateDownloader.Updates = $UpdatesToDownload # Download the updates. Write-Verbose 'Downloading driver updates...' $UpdateDownloader.Download() } # Create a new update collection to hold the updates we want to install. $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl # Add downloaded updates to the update collection. $UpdatesToDownload | ForEach-Object { if ($_.IsDownloaded) { # Add the update to the update collection if it has been downloaded. $UpdatesToInstall.Add($_) | Out-Null } } # If there are updates to install, install them. if (($UpdatesToInstall.count -gt 0) -or (![string]::IsNullOrEmpty($UpdatesToInstall))) { # Create an update installer. $UpdateInstaller = $UpdaterSession.CreateUpdateInstaller() # Add the updates to the installer. $UpdateInstaller.Updates = $UpdatesToInstall # Install the updates. Write-Verbose 'Installing driver updates...' $InstallationResult = $UpdateInstaller.Install() # If we need to reboot flag that information. if ($InstallationResult.RebootRequired) { Write-Warning 'Reboot required to complete driver updates.' Ninja-Property-Set driverUpdateRebootRequired 1 # Adjust for RMM } # Output the results of the installation. ## Result codes: 0 = Not Started, 1 = In Progress, 2 = Succeeded, 3 = Succeeded with Errors, 4 = Failed, 5 = Aborted ## We consider 1, 2, and 3 to be successful here. if (($InstallationResult.ResultCode -eq 1) -or ($InstallationResult.ResultCode -eq 2) -or ($InstallationResult.ResultCode -eq 3)) { Write-Verbose 'Driver updates installed successfully.' Ninja-Property-Set driverUpdateRebootRequired 0 # Adjust for RMM Ninja-Property-Set driverUpdateLastRun (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss') # Adjust for RMM Ninja-Property-Set driverUpdateNumberInstalledOnLastRun $UpdatesToInstall.Count # Adjust for RMM } else { Write-Warning "Driver updates failed to install. Result code: $($InstallationResult.ResultCode.ToString())" exit 1 } } } } catch { Write-Error $_.Exception.Message exit 1 } View on GitHub THE RESULTS You can set this up to run on a schedule - we run this script immediately on machine onboarding and then every 7 days on a Tuesday. This doesn't always have anything to do as our Windows Update run usually handles these updates, but it's a good way to ensure that we're always up to date with the latest drivers from Microsoft Update. Tags: * Updates * NinjaOne * Custom Fields * PowerShell Older Entries Useful Links and Communities * CIPP * MSPGeek * WinAdmins More Homotechsual * RSS Feed * Mastodon * GitHub * Twitter Other Useful Blogs * CyberDrain * MSPP * Gavsto * TechFoundry * MendyOnline Copyright © 2023 Mikey O'Toole | Built with Docusaurus version 3.0.1