New Tool: Delivery Optimization Monitor

Delivery Optimization Monitor is a tool for viewing Delivery Optimization data on the local or a remote PC.

It is based on the built-in Delivery Optimization UI in Windows 10 but allows you to view data graphically from remote computers as well.

The tool uses the Delivery Optimization PowerShell cmdlets built in to Windows 10 to retrieve and display DO data, including stats and charts for the current month, performance snapshot data and data on any current DO jobs.


  • A supported version of Windows 10 (1703 onward)
  • PowerShell 5 minimum
  • .Net Framework 4.6.2 minimum
  • PS Remoting enabled to view data from remote computers.

This WPF tool is coded in Xaml and PowerShell and uses the MahApps.Metro and LiveCharts open source libraries.


Download the tool from the Technet Gallery.


To use the tool, extract the ZIP file, right-click the Delivery Optimization Monitor.ps1 and run with PowerShell.

To run against the local machine, you must run the tool elevated. To do so, create a shortcut to the ps1 file. Edit the properties of the shortcut and change the target to read:

PowerShell.exe -ExecutionPolicy Bypass -File “<pathtoPS1file>”

Right-click the shortcut and run as administrator, or edit the shortcut properties (under Advanced) to run as administrator.

For completeness, you can also change the icon of the shortcut to the icon file included in the bin directory.

Delivery Optimization Statistics

There are 3 tabs – the first displays DO data for the current month together with charts for download and upload statistics.

The second tab displays PerfSnap data and the third displays any current DO jobs.

Shout Out

Shout out to Kevin Rahetilahy over at for blogging about LiveCharts in PowerShell.

Source Code

Source code can be found on GitHub.

Creating ADR Deployments in SCCM with PowerShell

Today I needed to create a number of deployments for Software Update Automatic Deployment Rules in SCCM, so I turned to PowerShell and used the New-CMAutoDeploymentRuleDeployment cmdlet available in the ConfigurationManager module. It works well enough, however there are a couple of options that the cmdlet cannot set, namely:

  • If software updates are not available on distribution point in current, neighbour or site boundary groups, download content from Microsoft Updates
  • If any update in this deployment requires a system restart, run updates deployment evaluation cycle after restart

Turns out that these can easily be set though by manipulating the XML deployment template in the object returned by the cmdlet. You can actually set all the deployment properties that way if you wanted, so long as you know the parameters and values from the deployment template XML.

Here is an example that creates the ADR deployments for an array of collections and also sets the two options above:

# ADR name
$ADRName = "Windows 10 Updates"

# Collections to create deployments for
$Collections = @(
    'SUP - Pilot - ABC - All'
    'SUP - Pilot - XYZ - All'
    'SUP - Production - ABC - All'
    'SUP - Production - XYZ - All'


# Import ConfigMgr Module
Import-Module $env:SMS_ADMIN_UI_PATH.Replace('i386','ConfigurationManager.psd1')
$SiteCode = (Get-PSDrive -PSProvider CMSITE).Name
Set-Location ("$SiteCode" + ":")

# Get the ADR
$ADR = Get-CMAutoDeploymentRule -Name $ADRName

# Create the deployments
Foreach ($Collection in $Collections)
    # Create the deployment
    $Params = @{
        CollectionName = $Collection
        EnableDeployment = $true
        SendWakeupPacket = $false
        VerboseLevel = 'OnlySuccessAndErrorMessages'
        UseUtc = $true
        AvailableTime = 2
        AvailableTimeUnit = 'Days'
        DeadlineImmediately = $true
        UserNotification = 'DisplaySoftwareCenterOnly'
        AllowSoftwareInstallationOutsideMaintenanceWindow = $true
        AllowRestart = $false
        SuppressRestartServer = $true
        SuppressRestartWorkstation = $true
        WriteFilterHandling = $true
        NoInstallOnRemote = $false 
        NoInstallOnUnprotected = $false
        UseBranchCache = $true
    $null = $ADR | New-CMAutoDeploymentRuleDeployment @Params

    # Update the deployment with some additional params not available in the cmdlet
    $ADRDeployment = Get-CMAutoDeploymentRuleDeployment -Name $ADRName -Fast | where {$_.CollectionName -eq $Collection}
    [xml]$DT = $ADRDeployment.DeploymentTemplate
    # If software updates are not available on distribution point in current, neighbour or site boundary groups, download content from Microsoft Updates
    $DT.DeploymentCreationActionXML.AllowWUMU = "true" 
    # If any update in this deployment requires a system restart, run updates deployment evaluation cycle after restart
    If ($DT.DeploymentCreationActionXML.RequirePostRebootFullScan -eq $null)
        $NewChild = $DT.CreateElement("RequirePostRebootFullScan")
    $DT.DeploymentCreationActionXML.RequirePostRebootFullScan = "Checked" 
    $ADRDeployment.DeploymentTemplate = $DT.OuterXml

Monitor Content Downloads Between an SCCM Distribution Point and a Client

Sometimes you want to monitor the progress of a content download on an SCCM client from a distribution point. You can use the Get-BitsTransfer PowerShell cmdlet, but it doesn’t currently support running on remote computers, so I wrapped the cmdlet in a bit of extra code that lets you get Bits transfer information from a remote computer, and adds a couple of extra values like the transfer size in megabytes and gigabytes as well as a percent complete value. Run it while there’s an active transfer to monitor the progress.

Simply provide a computer name like so:

Get-BitsTransfers -ComputerName PC001
Function Get-BitsTransfers {


    Invoke-Command -ComputerName $ComputerName -ScriptBlock {
        $BitsTransfers = Get-BitsTransfer -AllUsers 
        Foreach ($BitsTransfer in $BitsTransfers)
                DisplayName = $BitsTransfer.DisplayName
                JobState = $BitsTransfer.JobState
                OwnerAccount = $BitsTransfer.OwnerAccount
                FilesTotal = $BitsTransfer.FilesTotal
                FilesTransferred = $BitsTransfer.FilesTransferred
                BytesTotal = $BitsTransfer.BytesTotal
                MegaBytesTotal = [Math]::Round(($BitsTransfer.BytesTotal / 1MB),2)
                GigaBytesTotal = [Math]::Round(($BitsTransfer.BytesTotal/ 1GB),2)
                BytesTransferred = $BitsTransfer.BytesTransferred
                PercentComplete = [Math]::Round((100 * ($BitsTransfer.BytesTransferred / $BitsTransfer.BytesTotal)),2)
                CreationTime = $BitsTransfer.CreationTime
                TransferCompletionTime = $BitsTransfer.TransferCompletionTime

    } -HideComputerName


New Tool: ConfigMgr Client Notification

Today I whipped-up a very simple tool for ConfigMgr admins and support staff. It allows you to send client notifications (using the so-called fast channel), such as downloading the computer policy, collecting hardware inventory, checking compliance etc, to remote computers from your local workstation independently of the ConfigMgr console.


The tool connects to your ConfigMgr site server using a Cimsession and PSSession, so you need WsMan operational in your environment. You simply provide some computer name/s in the text box, enter your site server name, select which client notification you want to send and click GO. The tool will get the online status of the clients from the SMS Provider to give you an indication of which systems will receive the client notification. Then it will trigger the client notification on online systems from the site server.

The tool is coded in PowerShell / Xaml and uses the MahApps Metro libraries for WPF styling.


Download the tool from here.


I decided not to package the tool this time but just to release the files as they are, so if you need to tweak something for it to work in your environment, such as a non-default WsMan port, you can do that. Download and extract the zip file, right-click the ‘ConfigMgr Client Notification Tool.ps1’ and run with PowerShell.


– Dot Net 4.6.2 minimum

  • PowerShell 5 minimum

  • WSMan remote access to the ConfigMgr Site server on the default port

  • Appropriate RBAC permissions for performing client operations

  • A version of ConfigMgr that supports the client notifications

Feel free to leave any feedback.

Querying for Devices in Azure AD and Intune with PowerShell and Microsoft Graph

Recently I needed to get a list of devices in both Azure Active Directory and Intune and I found that using the online portals I could not filter devices by the parameters that I needed. So I turned to Microsoft Graph to get the data instead. You can use the Microsoft Graph Explorer to query via the Graph REST API, however, the query capabilities of the API are still somewhat limited. To find the data I needed, I had to query the Graph REST API using PowerShell, where I can take advantage of the greater filtering capabilities of PowerShell’s Where-Object.

To use the Graph API, you need to authenticate first. A cool guy named Dave Falkus has published a number of PowerShell scripts on GitHub that use the Graph API with Intune, and these contain some code to authenticate with the API. Rather than re-invent the wheel, we can use his functions to get the authentication token that we need.

First, we need the AzureRM or Azure AD module installed as we use the authentication libraries that are included with it.

Next, open one of the scripts that Dave has published on GitHub, for example here, and copy the function Get-AuthToken into your script.

The also copy the Authentication code region into your script, ie the section between the following:

#region Authentication

If you run this code it’ll ask you for an account name to authenticate with from your Azure AD. Once authenticated, we have a token we can use with the Graph REST API saved as a globally-scoped variable $authToken.

Get Devices from Azure AD

To get devices from Azure AD, we can use the following function, which I take no credit for as I have simply modified a function written by Dave.

Function Get-AzureADDevices(){


$graphApiVersion = "v1.0"
$Resource = "devices"
$QueryParams = ""

    try {

        $uri = "$graphApiVersion/$($Resource)$QueryParams"
        Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"



In the $graphAPIVersion parameter, you can use the current version of the API.

Now we can run the following code, which will use the API to return all devices in your Azure AD and save them to them a hash table which organizes the results by operating system version.

# Return the data
$ADDeviceResponse = Get-AzureADDevices
$ADDevices = $ADDeviceResponse.Value
$NextLink = $ADDeviceResponse.'@odata.nextLink'
# Need to loop the requests because only 100 results are returned each time
While ($NextLink -ne $null)
    $ADDeviceResponse = Invoke-RestMethod -Uri $NextLink -Headers $authToken -Method Get
    $NextLink = $ADDeviceResponse.'@odata.nextLink'
    $ADDevices += $ADDeviceResponse.Value

Write-Host "Found $($ADDevices.Count) devices in Azure AD" -ForegroundColor Yellow
$ADDevices.operatingSystem | group -NoElement

$DeviceTypes = $ADDevices.operatingSystem | group -NoElement | Select -ExpandProperty Name
$AzureADDevices = @{}
Foreach ($DeviceType in $DeviceTypes)
    $AzureADDevices.$DeviceType = $ADDevices | where {$_.operatingSystem -eq "$DeviceType"} | Sort displayName

Write-host "Devices have been saved to a variable. Enter '`$AzureADDevices' to view."

It will tell you how many devices it found, and how many devices there are by operating system version / device type.

2018-10-22 16_06_14-Windows PowerShell ISE

We can now use the $AzureADDevices hash table to query the data as we wish.

For example, here I search for an iPhone that belongs to a particular user:

$AzureADDevices.Iphone | where {$_.displayName -match 'nik'}

Here I am looking for the count of Windows devices that are hybrid Azure AD joined, and display the detail in the GridView.

($AzureADDevices.Windows | where {$_.trustType -eq 'ServerAd'}).Count
$AzureADDevices.Windows | where {$_.trustType -eq 'ServerAd'} | Out-GridView

And here I’m looking for all MacOS devices that are not compliant with policy.

($AzureADDevices.MacOS | where {$_.isCompliant -ne "True"}) | Out-GridView

Get Devices from Intune

To get devices from Intune, we can take a similar approach. Again no credit for this function as its modified from Dave’s code.

Function Get-IntuneDevices(){


# Defining Variables
$graphApiVersion = "v1.0"
$Resource = "deviceManagement/managedDevices"

try {

    $uri = "$graphApiVersion/$Resource"
    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value


    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"



Running the following code will return all devices in Intune and save them to a hash table again organised by operating system.

$MDMDevices = Get-IntuneDevices

Write-Host "Found $($MDMDevices.Count) devices in Intune" -ForegroundColor Yellow
$MDMDevices.operatingSystem | group -NoElement

$IntuneDeviceTypes = $MDMDevices.operatingSystem | group -NoElement | Select -ExpandProperty Name
$IntuneDevices = @{}
Foreach ($IntuneDeviceType in $IntuneDeviceTypes)
    $IntuneDevices.$IntuneDeviceType = $MDMDevices | where {$_.operatingSystem -eq "$IntuneDeviceType"} | Sort displayName

Write-host "Devices have been saved to a variable. Enter '`$IntuneDevices' to view."

Now we can query data using the $IntuneDevices variable.

Here I am querying for the count of compliant and non-compliant iOS devices.

$IntuneDevices.iOS | group complianceState -NoElement

Here I am querying for all non-compliant iOS devices, specifying the columns I want to see, sort the results and outputting into table format.

$IntuneDevices.iOS |
    where {$_.complianceState -eq "noncompliant"} |
    Select userDisplayName,deviceName,imei,managementState,complianceGracePeriodExpirationDateTime |
    Sort userDisplayName |

All Windows devices sorted by username:

$IntuneDevices.Windows | Select userDisplayName,deviceName | Sort userDisplayName

Windows devices managed by SCCM:

$IntuneDevices.Windows | where {$_.managementAgent -eq "ConfigurationManagerClientMdm"} | Out-GridView

Windows devices enrolled using Windows auto enrollment:

$IntuneDevices.Windows | where {$_.deviceEnrollmentType -eq "windowsAutoEnrollment"} | Out-GridView

Windows devices enrolled by SCCM co-management:

$IntuneDevices.Windows | where {$_.deviceEnrollmentType -eq "windowsCoManagement"} | Out-GridView

You can, of course, expand this into users and other resource types, not just devices. You just need the right URL construct for the data type you want to query.

Intune Client-Side Logs in Windows 10

Note to self (and anyone interested!) about the client-side location of logs and management components of Intune on a Windows 10 device.

Diagnostic Report

A diagnostic report can be generated client-side from Settings > Access Work and School > Connected to <Tenant>’s Azure AD > Info > Create Report

The report will be saved to:

C:\Users\Public\Public Documents\MDMDiagnostics\MDMDiagReport.html

Intune Management Extension

Information on the parameters for the IME can be found in the registry:


The MSI itself can be found here, together with an installer log:


Note: if you disconnect a device from Azure AD and rejoin it again, you will need to reinstall the IME as it will have a different device identifier.

IME logs can be found here:


The logs are:

  • AgentExecutor
  • ClientHealth
  • IntuneManagementExtension

Script Execution

When a PowerShell script is run on the client from Intune, the scripts and the script output will be stored here, but only until execution is complete:

C:\Program files (x86)\Microsoft Intune Management Extension\Policies\Scripts

C:\Program files (x86)\Microsoft Intune Management Extension\Policies\Results

A transcript of the script execution can be found underneath C:_showmewindows (a hidden folder)

The full content of the script will also be logged in the IntuneManagementExtension.log (be careful of sensitive data in scripts!)

The error code and result output of the script can also be found in the registry:


Event Logs

There are a couple of MDM event logs which can be found here:

Applications and Services Logs > Microsoft > Windows > DeviceManagement-Enterprise-Diagnostics-Provider


The IME runs as a service called “Microsoft Intune Management Extension”. You can restart this to force a check for new policies.

Scheduled Task

The IME runs a health evaluation every day as a scheduled task, and logs the results in the ClientHealth.log:

Microsoft > Intune > Intune Management Extension Health Evaluation

If you know of any other log locations, please let me know!

Lots of great info on the IME by Oliver Kieselbach here and here.

Create a Custom Splash Screen for a Windows 10 In-Place Upgrade

A while back I wrote a blog with some scripts that can be used to improve the user experience in a Windows 10 in-place upgrade. The solution included a simple splash screen that runs at the beginning of the upgrade to block the screen to the user and discourage interaction with the computer during the online phase of the upgrade. Since then, I made some improvements to the screen and styled it to look more like the built-in Windows update experience in Windows 10. Using this splash screen not only discourages computer interaction during the upgrade, but also creates a consistent user experience throughout the upgrade process, for a user-initiated upgrade.

The updated screen contains an array of text sentences that you can customise as you wish. Here is an example of what it could look like:

The splash screen is not completely foolproof in that it is still possible to use certain key combinations, like ctrl-alt-del and alt-tab etc, but the mouse cursor is hidden and mouse buttons will do nothing. The intention is simply to discourage the user from using the computer during the online phase. If the computer is locked, it will display the splash screen again when unlocked. If you wish to block user interaction completely, you might consider a more hardcore approach like this or this.

To use the splash screen, download all the files in my GitHub repository here (including the bin directory). Create a standard package in ConfigMgr containing the files (no program needed) and distribute. Then add a Run PowerShell Script step in the beginning of your in-place upgrade task sequence that looks like the following (reference the package you created):


Once the splash screen has been displayed, the task sequence will move on to the next step – the screen will not block the task sequence.

How does it work?

The Invoke-PSScriptAsUser.ps1 simple calls the Show-OSUpgradeBackground.ps1 and runs it in the context of the currently logged-on user so that the splash screen will be visible to the user (task sequences run in SYSTEM context so this is necessary).

The Show-OSUpgradeBackground.ps1 determines your active screens, creates a runspace for each that calls PowerShell.exe and runs the Create-FullScreenBackground.ps1 for each screen.

The Create-FullScreenBackground.ps1  does the main work of displaying the splash screen. It will hide the task bar, hide the mouse cursor and display a full screen window in the Windows 10 update style. I’ve used the excellent MahApps toolkit to create the progress ring. The text displayed in the screen can be defined by placing short sentences in the $TextArray variable. The dispatcher timer will cycle through each of the these every 10 seconds (or whatever value you set) ending with a final sentence “Windows 10 Upgrade in Progress” which will stay on the screen until the computer is restarted into the next phase of the upgrade.

You can test the splash screen before deploying it simply by running the Show-OSUpgradeBackground.ps1 script.

Remember to deselect the option Show task sequence progress in the task sequence deployment to avoid having the task sequence UI show up on top of the window.