Skip to main content

11 posts tagged with "PowerShell"

View All Tags

· 4 min read

Secure Application Model

For this post you'll want a secure application model Azure AD application configured. Use these instructions from Gavin Stone's blog.

CIPP Inspired

The code in this script was inspired by functionality in the CyberDrain Improved Partner Portal (CIPP). Need to manage M365 tenants - CIPP is a fantastic open-source solution for M365 management for single tenants and Microsoft CSP Partners alike.

Background information

In my quest to stop having to use Windows Sandbox or VMs to access Exchange Online via PowerShell (we have WinRM basic auth disabled) I've been looking at solutions using the Exchange Online REST API to run commands against our customer tenants. The result of my efforts is a pretty complex script that introduces a method to run any PowerShell script in the context of a set of customer tenants.


You need to have configured the Secure Application Model according to the instructions linked above, you will need your SAM tokens etc accessible for this guide.

Securing Credentials in Scripts

Do not store credentials directly in scripts. You should utilise services like Azure KeyVault or other PAM solutions to secure credentials used in scripts, including this one.

· One min read

I'm the author of the NinjaOne PowerShell module so when I was looking for a quick way to identify duplicate devices I turned to that module and the NinjaOne API to find a solution.

About this script

This script is a quick way to identify duplicate devices in your NinjaOne instance. It can return a full listing of all devices grouped by serial number, it can also just return the ID, last contact date and serial number for the device with the oldest last contact date.

The script

#Requires -Module NinjaOne
#Requires -Version 7
param (
# Returns all devices with duplicates.
[Parameter(ParameterSetName = 'All', Mandatory)]
# Returns the duplicate devices only.
[Parameter(ParameterSetName = 'Duplicates', Mandatory)]

try {
$DuplicateDevices = Get-NinjaOneDevices -detailed | Group-Object { $_.system.serialNumber } | Where-Object { $_.count -gt 1 -and ($ -ne '$(DEFAULT_STRING)' -and $ -ne 'Default string') }
if ($All) {
$Output = $DuplicateDevices | ForEach-Object { $_ | Select-Object -ExpandProperty group | Sort-Object $_.lastContact }
} elseif ($Duplicates) {
$Output = $DuplicateDevices | ForEach-Object { $_ | Select-Object -ExpandProperty group | Sort-Object $_.lastContact | Select-Object -First 1 -Property id, lastContact, @{ name = 'serialNumber'; expression = { $_.system.serialNumber } } }
if ($Output) {
return $Output
} else {
Write-Warning 'No duplicate devices found or an unanticipated error occurred.'
} catch {
Write-Error 'Please ensure the NinjaOne PowerShell module is correctly installed and you have run the "Connect-NinjaOne" command and connected to NinjaOne.'
exit 1

At this time it's not possible to remove devices via the NinjaOne API so this script will only return the duplicate devices - you'll have to arrange to remove these devices manually.

If you want all devices with duplicates:

Get-NinjaOneDuplicateDevices -All

or just the older duplicated devices:

Get-NinjaOneDuplicateDevices -Duplicates

· 2 min read

Did you know, when you have a retention policy configured for OneDrive in Microsoft 365 you lose the ability to recursively delete folders. The OneDrive client often handles this by recursing through the folder tree to remove the online copies but sometimes that doesn't work and in those cases you can end up stuck with a folder tree that you would have to manually recurse to remove the offending folders by first emptying them entirely before OneDrive will let you delete them.

Well that's never going to happen ;-)

Enter PowerShell.

Required Modules

You'll need the PnP.PowerShell module for this script. Install with Install-Module 'PnP.PowerShell'

The script

param (
# OneDrive host name e.g. ''
# Site path e.g. '/personal/satya_nadella_microsoft_com' must start with '/'.
# Folder path e.g. '/Documents/Documents/PowerShell/Modules' must start with '/'.

# Setup some configuration variables.
$SiteURL = $OneDriveHost + $SitePath
$FolderSiteRelativeURL = $SitePath + $FolderPath

# Connect to the site with the PnP.PowerShell module.
Connect-PnPOnline -Url $SiteURL -Interactive
$Web = Get-PnPWeb
$Folder = Get-PnPFolder -Url $FolderSiteRelativeURL

# Function to recursively remove files and folders from the path given.
Function Clear-PnPFolder([Microsoft.SharePoint.Client.Folder]$Folder) {
$InformationPreference = 'Continue'
If ($Web.ServerRelativeURL -eq '/') {
$FolderSiteRelativeURL = $Folder.ServerRelativeUrl
} Else {
$FolderSiteRelativeURL = $Folder.ServerRelativeUrl.Replace($Web.ServerRelativeURL, [string]::Empty)
# First remove all files in the folder.
$Files = Get-PnPFolderItem -FolderSiteRelativeUrl $FolderSiteRelativeURL -ItemType File
ForEach ($File in $Files) {
# Delete the file.
Remove-PnPFile -ServerRelativeUrl $File.ServerRelativeURL -Force -Recycle
Write-Information ("Deleted File: '{0}' at '{1}'" -f $File.Name, $File.ServerRelativeURL)
# Second loop through sub folders and remove them - unless they are "special" or "hidden" folders.
$SubFolders = Get-PnPFolderItem -FolderSiteRelativeUrl $FolderSiteRelativeURL -ItemType Folder
Foreach ($SubFolder in $SubFolders) {
If (($SubFolder.Name -ne 'Forms') -and (-Not($SubFolder.Name.StartsWith('_')))) {
# Recurse into children.
Clear-PnPFolder -Folder $SubFolder
# Finally delete the now empty folder.
Remove-PnPFolder -Name $SubFolder.Name -Folder $Site + $FolderSiteRelativeURL -Force -Recycle
Write-Information ("Deleted Folder: '{0}' at '{1}'" -f $SubFolder.Name, $SubFolder.ServerRelativeURL)
$InformationPreference = 'SilentlyContinue'
# Call the function to empty folder if it exists.
if ($null -ne $Folder) {
Clear-PnPFolder -Folder $Folder
} Else {
Write-Error ("Folder '{0}' not found" -f $FolderSiteRelativeURL)

Invoking the script

The script is looking for three parameters, an example invocation to remove a synced "Documents/PowerShell/Modules" folder in Satya Nadella's OneDrive would be:

Double the Documents

You'll notice below that the /Documents folder in the path is doubled. This is because the first folder in the OneDrive path is always /Documents then your path starts. In this example we're removing the /Documents/PowerShell/Modules folder.

Remove-OneDriveFolderRecursively -OneDriveHost '' -SitePath '/personal/satya_natella_microsoft_com' -FolderPath '/Documents/Documents/PowerShell/Modules'

Hope this helps!

This post was inspired by a script posted on Microsoft's TechCommunity here...

· 6 min read

This post uses code from CyberDrain

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

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

· 5 min read

Background information

The NinjaOne agent gives you the ability to customise your system tray and add things like about text, email links, a help request form, a URL and other things. Unfortunately HaloPSA doesn't currently support hooking into NinjaOne's help request form (don't worry, we're talking to NinjaOne and Halo Service Solutions about this!) so we're left with a bit of a workaround as a solution.

· 7 min read

About this script

HaloPSA is improving at a phenomenal rate - one of the latest enhancements relates to embedding the "customer portal" inside Teams for easier access by customers - this is early days for this enhancement to Halo and at present there isn't really a way to push this tab into customer environments... Until now!

Using the script below you can push any website as a tab to your customers' Teams environments. First a couple of configuration pre-requisites.

  1. You'll need to have setup the Secure App Model (Thanks Gav for the superb write-up!)
  2. You'll need to know the name of the Team and Channel you want to deploy the tab - currently the script will use the same details for all customers!

· 4 min read
Save yourself the headache - use the Microsoft Store on Windows 11

If you're using or managing this for Windows 11 you can now deploy Adobe Acrobat Reader DC using the Microsoft Store. This is preferable for many reasons mostly because this way of doing it with IntuneWin files is a complete pain in the ass.

About this script

If you're not familiar with the IntuneWin format and what it's used for/when it's used there's some good background reading from Microsoft here: Prepare a Win32 app to be uploaded to Microsoft Intune | Microsoft Docs.

In short, it's a format designed to package Windows application installers for deployment with Intune.

This post contains a script which downloads the latest version of the Adobe Reader DC installer and turns it into an IntuneWin package for Intune deployment.

· One min read

I don't handle Base64 strings very often - when I do I find myself searching for instructions every time. So I finally decided to add two small helper functions to my PowerShell profile.

function ConvertFrom-Base64String ([String]$InputString, [System.Text.Encoding]$Encoding) {
$DecodedString = $Encoding.GetString([System.Convert]::FromBase64String($InputString))

function ConvertTo-Base64String ([String]$InputString, [System.Text.Encoding]$Encoding) {
$StringBytes = $Encoding.GetBytes($InputString)
$EncodedString = [System.Convert]::ToBase64String($StringBytes)

You can quickly edit your PowerShell profile file by typing code $PROFILE into your PowerShell window - this will pop up VS Code with your profile ready to edit.