Cloud Native:
As we are discussing more and more about going cloud native on security and management, there are several practical challenges encounted by the organisations in their cloud native journey. In this post, I will talk about Windows Updates.
WUfB Reports in Log Analytics:
While WUfB provides significant benefits in ease of managing, obtaining updates directly from cloud, reporting has been a significant challenge. Initial Update Compliance reporting in Log Analytics is now evolved into Windows Update for Business reports. This is an offering from Windows Servicing team that helps you monitor, report and anlayze on security, quality, driver, and feature updates for Windows 11 and Windows 10 devices. This is included as part of Windows 10 and Windows 11 licenses and is not dependent on the management tool (ConfigMgr or Intune or GPO or any Third-Party management solution)
Challenges and Workaround:
As I was discussing with multiple Intune customers on challenges with existing Windows Update for Bussiness reports in Azure Monitor Log Analytics, one of the problem statement seems to be the lack of management information of the devices and slicing the data based on Entra groups. In this post, I will discuss a possible workaround (with PowerShell scripts) to integrate the Windows Update for Business reports data with device information from Intune and also disect the data based on Entra groups.
The solution broadly consists of below steps:
- Connect to Graph and get all managed Windows devices from Intune
- Export required properties of the managed devcies.
- Write to a custom Log Analytics Table
- Use it along with existing WUfB reports
Script:
In this script, I’m focusing on exporting below properties for each managed windows device to Log Analytics workspace which hosts the Windows Update for Business reports workbook.
- DeviceName
- IntuneDeviceId
- AzureADDeviceId
- CoManaged (True or False)
- UpdateManagedBy (if Co-Managed = True; Intune or ConfigMgr)
- ScopeTags (To filter devices based on Scope Tags which is equivalent to Entra Groups)
- LastSyncDateTime (Intune Last Sync time)
- SkuFamily (OS Sku)
Configure the $OffsetHours in Line #10
. $OffsetHours defines the amount of data to be processed i.e the script process the devices which have last sync’ed in defined hours. When you run for first time, do a full sync by removing the $OffsetHours in Line #80
. After initial full sync, schedule the script to be executed once in every few hours depending on the need.
Configure the Log Analytics workspace id and primary/secodary key in Line #13 and #14
. Also configure the table name in Line #16
, if you wish to.
Modify the script block within region: ProcessIntuneData based on the properties you like to extract. If you want to use any of the HW inventory properties, the script needs modification. Check my previous post on obtaining HW Inventory data.
On executing the script, you will see success message, if the post to Log Analytics workspace is successful.
When you query the Log Analytics logs, you will see new table is created but it takes 20 seconds to 3 minutes on average to see data while the first time it takes up 15 minutes.
Querying the table after few minutes..
Now that you have the data handy, you can join the data with other tables and create your own queries and use scope tag assigned to a particular Entra group to limit the results.
I will follow up with a post, once I learn to edit workbooks 😊
Note: In all my sample scripts, I use delegated permissions. If you to run the script unattended in any automation tool, modify the script to levarge client app registration along with client secret – reference post
This script is based on Data Collector API which is deprecated but valid till 9/14/2026. I will update the script once I learn about the Log Ingestion API
<#
DISCLAIMER STARTS
THIS SAMPLE CODE IS PROVIDED FOR THE PURPOSE OF ILLUSTRATION ONLY AND IS NOT INTENDED TO BE USED IN A PRODUCTION ENVIRONMENT. THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED IS "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE."
DISCLAIMER ENDS
#>
#Set Variables
#$Now = Get-Date -Format "dd-MM-yyyy-HH-mm-ss"
#$LogFile = "C:\Windows\Temp\DeviceHWInfo_Update_Log_$($Now).log" #Use Logging and un-comment "Add-Content" command lines if running locally in server.
$OffsetHours = 4
#region LogAnalytics
$CustomerId = "" #Workspace Id
$SharedKey = "" #Input Primary or Secondary key
#$global:Proxy = "" #Use only if needed to send from a device connected to On-Premises network
$WUfBTable = "ucclient_Intune" #Set once and do not change later.
If (!$WUfBTable -or !$SharedKey -or !$CustomerId){
"Invalid Variables. Exiting..."; Exit
}
#Do Not Make Any Changes Below
$TimeStampField = ""
Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource)
{
$xHeaders = "x-ms-date:" + $date
$stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource
$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
$keyBytes = [Convert]::FromBase64String($sharedKey)
$sha256 = New-Object System.Security.Cryptography.HMACSHA256
$sha256.Key = $keyBytes
$calculatedHash = $sha256.ComputeHash($bytesToHash)
$encodedHash = [Convert]::ToBase64String($calculatedHash)
$authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash
return $authorization
}
Function Post-LogAnalyticsData ($customerId, $sharedKey, $json, $logType)
{
$method = "POST"
$contentType = "application/json"
$resource = "/api/logs"
$rfc1123date = [DateTime]::UtcNow.ToString("r")
$contentLength = $json.Length
$signature = Build-Signature `
-customerId $customerId `
-sharedKey $sharedKey `
-date $rfc1123date `
-contentLength $contentLength `
-method $method `
-contentType $contentType `
-resource $resource
$uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"
$headers = @{
"Authorization" = $signature;
"Log-Type" = $logType;
"x-ms-date" = $rfc1123date;
"time-generated-field" = $TimeStampField;
}
$response = Invoke-WebRequest -Uri $uri -Proxy $global:Proxy -Method $method -ContentType $contentType -Headers $headers -Body $json -UseBasicParsing -TimeoutSec 900
return $response.StatusCode
}
#endregion
$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
#Connect to Microsoft Graph using delegated permissions
Write-Output "Connecting Microsoft Graph..."
Connect-MgGraph -Scopes DeviceManagementManagedDevices.Read.All,DeviceManagementConfiguration.Read.All,DeviceManagementRBAC.Read.All
$CutOffDate = (Get-Date).AddHours(-$OffsetHours)
$CutOffDate = ([string](Get-Date $CutOffDate -Format "yyyy-MM-dd HH:mm:ss") -replace " ","T") + "z"
#Get List of Managed Devices. Here the platform is scoped to Android.
Write-Output "Getting List of Managed Devices updated in last $OffsetHours Hours..."
$ManagedDevices = Get-MgbetaDeviceManagementManagedDevice -Filter "operatingSystem eq 'Windows' and lastSyncDateTime ge $CutOffDate" -All
$IntuneScopeTags = Get-MgBetaDeviceManagementRoleScopeTag
#Get Total Devices Count
$TotalDevices = $ManagedDevices.count
#Initialize few variables
$STResults = @()
$PSObject = @()
$Set = 1
#Loop all devices to get Scope Tag information. Currently JSON batching supports only 20 requests at a time. So breaking down the total devices by set of 20 each and getting the Scope Tag Information and storing the output in $STResults array.
Write-Output "Getting Scope Tag Information of all managed devices..."
For ($n = 1; $n -le $TotalDevices; $n++){
Write-Progress -Activity "Getting Scope Tag Information of all managed devices..." -Status "Processing $n out of $TotalDevices" -PercentComplete $($n*100/$TotalDevices)
$id = $n - 1
$DeviceId = $ManagedDevices[$id].id
$PSObject += [PSCustomObject]@{
id = "$id"
method = "GET"
url = "/deviceManagement/managedDevices/$($DeviceId)"
}
if (($n -eq $($Set*20)) -or ($n -eq $TotalDevices)){
$BatchRequestBody = [PSCustomObject]@{requests = $PSObject }
$JSONRequests = $BatchRequestBody | ConvertTo-Json -Depth 4
$STResults += Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/beta/$batch' -Body $JSONRequests -ContentType 'application/json' -ErrorAction Stop
$PSObject = @()
$Set ++
}
}
#region ProcessIntuneData
Write-Output "Creating Update Status Report..."
#Add-Content -Path $LogFile -Value "IntuneDeviceId,AzureADDeviceId,DeviceName,SkuFamily,CoManaged,UpdateManagedBy,ScopeTags,LastSyncDateTime"
For ($n = 1; $n -le $TotalDevices; $n++){
Write-Progress -Activity "Creating Update Status Report..." -Status "Processing $n out of $TotalDevices" -PercentComplete $($n*100/$TotalDevices)
$id = $n - 1
$DeviceName = $ManagedDevices[$id].DeviceName
$IntuneDeviceId = $ManagedDevices[$id].Id
$AzureADDeviceId = $ManagedDevices[$id].AzureADDeviceId
If ($ManagedDevices[$id].ConfigurationManagerClientEnabledFeatures.Inventory -eq $true){
$CoManaged = $true
} Else { $CoManaged = $false }
If ($CoManaged -eq $true -and $ManagedDevices[$id].ConfigurationManagerClientEnabledFeatures.WindowsUpdateForBusiness -eq $false){
$UpdateManagedBy = "CM"
} Else { $UpdateManagedBy = "Intune" }
$ScopeTags = @()
$ScopeTagIds = $STResults.Responses[$id].body.roleScopeTagIds
Foreach ($ScopeTagId in $ScopeTagIds) { $ScopeTags += ($IntuneScopeTags | where {$_.Id -eq $ScopeTagId}).DisplayName}
$LastSyncDateTime = $ManagedDevices[$id].LastSyncDateTime
$SkuFamily = $ManagedDevices[$id].SkuFamily
$PSObject += [PSCustomObject]@{
DeviceName = $DeviceName
IntuneDeviceId = $IntuneDeviceId
AzureADDeviceId = $AzureADDeviceId
CoManaged = $CoManaged
UpdateManagedBy = $UpdateManagedBy
ScopeTags = $ScopeTags
LastSyncDateTime = $LastSyncDateTime
SkuFamily = $SkuFamily
}
#Add-Content -Path $LogFile -Value "$IntuneDeviceId,$AzureADDeviceId,$DeviceName,$SkuFamily,$CoManaged,$UpdateManagedBy,$ScopeTags,$lastSyncDateTime"
}
#endregion
$Json = $PSObject | ConvertTo-Json -Depth 32
$status = Post-LogAnalyticsData -customerId $CustomerId -sharedKey $SharedKey -json ([System.Text.Encoding]::UTF8.GetBytes($Json)) -logType $WUfBTable
if ($status -eq "200") {
"Successfully posted to log analytics"
} Else {"log analytics post error" }
$Stopwatch.Stop()
Write-Output "Time Taken for the script execution with batching:`n"
$Stopwatch.Elapsed
#Invoke-Item $LogFile
2 thoughts on “Intune: Enrich WUfB Log Analytics Reports with Intune Data and filter based on Scope tags”