Enhance Update Compliance Reporting with Azure Automation

In my last blog I provided an Intune Proactive remediations script that can be used to gather additional Windows Update information from your managed Windows devices and send the data to an Azure Log Analytics workspace to enhance your Windows Update reporting together with Microsoft’s Update Compliance solution.

In this blog, I will provide an Azure Automation Runbook that can be used to add even more Windows Update data to your LA workspace for Windows 10 and 11 devices. This is an abridged version of the runbook I use with my own Update Compliance solution and retrieves data from MS docs on:

  • Windows update history (ie history of security and non-security cumulative updates per Windows release)
  • Windows update support info (ie whether a Windows version and edition is currently in support and how long for)
  • Latest Windows updates (ie what are the most recent security and non-security updates per Windows release including n-1 and n-2 updates)
  • Documented Windows Update error codes and descriptions

The info is ‘scraped’ from MS docs pages, including:

After executing the runbook, you’ll have 4 new custom logs in your LA workspace:

SU_OSLatestUpdates_CL

This table contains each Windows version with the build number, KB, release date and update type of the latest security monthly ‘B’ update, the latest preview update and the latest out-of-band update (if applicable), including for n-1 and n-2 updates. ‘LatestUpdate’ here means the latest CU whether a ‘B’ or ‘C’ or OOB update, and ‘LatestRegularUpdate’ is specifically the monthly ‘B’ security update.

SU_OSSupportMatrix_CL

This table contains each Windows release with the release date, end of support date, support period, support days remaining, is it in support currently – per edition family, ie Home and Pro, or Enterprise and Education.

SU_OSUpdateHistory_CL

This table contains each cumulative update released for each Windows release including the KB number, build number, release date and update type.

SU_WUErrorCodes_CL

This table contains documented Windows Update error codes together with a category and friendly description.

Create the Azure Automation Runbook

I’ll assume you already have an Azure Automation account and Log Analytics workspace to work with. If you are already using Microsoft’s Update Compliance you can use the same Log Analytics workspace to avoid cross-workspace queries.

Create encrypted variables

In the Automation account, under Shared resources > Variables, create two encrypted variables called WorkspaceID and PrimaryKey. Copy the Workspace ID and the Primary Key of your Log Analytics workspace into the variables (get these from Settings > Agent management).

Create the Runbook

Create a new runbook with a super-original name and use the 5.1 runtime version.

In the Edit PowerShell Runbook window, paste in the contents of this script

No changes are required to the script, with the possible exception of line 695. This line controls how frequently the Windows Update error code information is pulled from MS docs. Because this info rarely changes, I only run it twice a month on days 7 and 21, just to prevent the data from ageing out of the workspace due to the data retention period. You may wish to change one of those values to today for the first run so you’ve got some data right away.

Save and Publish your runbook and then start it! The runbook only takes seconds to execute and after a successful job you should see http 200 status codes output for the posts to the LA workspace.

Assign a schedule to the runbook to keep the data up-to-date – I run mine every 4 hours because I also calculate device compliance with it, but otherwise a daily run might be sufficient.

Sample KQL queries

Once your data is available in the workspace, here are some example KQL queries you can use on the data.

Query the support info for Windows versions

SU_OSSupportMatrix_CL 
| summarize arg_max(TimeGenerated,*) by 
    Windows_Release=Windows_Release_s,
    Version=Version_s,
    EditionFamily=EditionFamily_s
| project 
    Windows_Release,
    Version,
    InSupport=InSupport_s,
    EditionFamily,
    StartDate=StartDate_s,
    EndDate=EndDate_s,
    SupportPeriodInDays=SupportPeriodInDays_d,
    SupportDaysRemaining=SupportDaysRemaining_d,
    TimeGenerated
| order by Windows_Release,Version,EditionFamily desc

Query the update history for Windows versions

SU_OSUpdateHistory_CL
| summarize arg_max(TimeGenerated,*) by Windows_Release_s,OSVersion_s,OSBuild_s
| project
    Windows_Release=Windows_Release_s,
    OSVersion=OSVersion_s,
    KB=KB_s,
    OSBaseBuild=OSBaseBuild_d,
    OSBuild=OSBuild_s,
    OSRevisionNumber=OSRevisionNumber_d,
    ReleaseDate=ReleaseDate_t,
    Type=case(Type_s == "Regular","Security 'B'",
        Type_s == "Preview","Non-Security Preview",
        Type_s),
    TimeGenerated
| order by Windows_Release desc,OSVersion desc,ReleaseDate desc

Query documented Windows update error codes and descriptions

SU_WUErrorCodes_CL
| summarize arg_max(TimeGenerated,*) by ErrorCode_s
| project
    ErrorCode=ErrorCode_s,
    Message,
    Description=Description_s,
    Category

Query the latest updates for each Windows version

SU_OSLatestUpdates_CL
| summarize arg_max(TimeGenerated,*) by Windows_Release_s,OSVersion_s
| project 
    TimeGenerated,
    Windows_Release=Windows_Release_s,
    OSBaseBuild=OSBaseBuild_d,
    OSVersion=OSVersion_s,
    LatestUpdate=LatestUpdate_s,
    LatestUpdateType=LatestUpdateType_s,
    LatestUpdateKB=LatestUpdate_KB_s,
    LatestUpdateReleaseDate=LatestUpdate_ReleaseDate_s,
    LatestRegularUpdate=LatestRegularUpdate_s,
    LatestRegularUpdateKB=LatestRegularUpdate_KB_s,
    LatestRegularUpdateReleaseDate=LatestRegularUpdate_ReleaseDate_s,
    LatestPreviewUpdate=LatestPreviewUpdate_s,
    LatestPreviewUpdateKB=LatestPreviewUpdate_KB_s,
    LatestPreviewUpdateReleaseDate=LatestPreviewUpdate_ReleaseDate_s,
    LatestOutofBandUpdate=LatestOutofBandUpdate_s,
    LatestOutofBandUpdateKB=LatestOutofBandUpdate_KB_s,
    LatestOutofBandUpdateReleaseDate=LatestOutofBandUpdate_ReleaseDate_s,
    LatestRegularUpdateLess1=LatestRegularUpdateLess1_s,
    LatestRegularUpdateLess1KB=LatestRegularUpdateLess1_KB_s,
    LatestRegularUpdateLess1ReleaseDate=LatestRegularUpdateLess1_ReleaseDate_s,
    LatestPreviewUpdateLess1=LatestPreviewUpdateLess1_s,
    LatestPreviewUpdateLess1KB=LatestPreviewUpdateLess1_KB_s,
    LatestPreviewUpdateLess1ReleaseDate=LatestPreviewUpdateLess1_ReleaseDate_s,
    LatestOutofBandUpdateLess1=LatestOutofBandUpdateLess1_s,
    LatestOutofBandUpdateLess1KB=LatestOutofBandUpdateLess1_KB_s,
    LatestOutofBandUpdateLess1ReleaseDate=LatestOutofBandUpdateLess1_ReleaseDate_s,
    LatestRegularUpdateLess2=LatestRegularUpdateLess2_s,
    LatestRegularUpdateLess2KB=LatestRegularUpdateLess2_KB_s,
    LatestRegularUpdateLess2ReleaseDate=LatestRegularUpdateLess2_ReleaseDate_s,
    LatestPreviewUpdateLess2=LatestPreviewUpdateLess2_s,
    LatestPreviewUpdateLess2KB=LatestPreviewUpdateLess2_KB_s,
    LatestPreviewUpdateLess2ReleaseDate=LatestPreviewUpdateLess2_ReleaseDate_s,
    LatestOutofBandUpdateLess2=LatestOutofBandUpdateLess2_s,
    LatestOutofBandUpdateLess2KB=LatestOutofBandUpdateLess2_KB_s,
    LatestOutofBandUpdateLess2ReleaseDate=LatestOutofBandUpdateLess2_ReleaseDate_s
| order by Windows_Release,OSVersion

Power BI

I’m a sucker for a nice Power BI report. Here’s a couple of examples of how you could report on this data:

Known Issues

Since MS docs update history does not contain updates for RTM releases, when a new feature update is released, there will be no update history reported until the release of the first CU.

A final note: Gary Blok has created a feedback item to make it easier to get Windows Release Information from Microsoft via an API. You can upvote it here: aka.ms/AAh5c1x

15 thoughts on “Enhance Update Compliance Reporting with Azure Automation

  1. Hello,

    i have tryed to deploy your solution
    https://docs.smsagent.blog/microsoft-endpoint-manager-reporting/windows-update-for-business-custom-reporting

    But after to run the first execution of this runbook, and second i have the same error

    “After the first execution, wait at least 5-10 minutes then execute the runbook again. There should be no further errors once the new data has been ingested.”

    but i have the same error 😦

    can you help me plz because beacause i need to use your powerbi 🙂

    Thx a lot in advance

    Romain HERRERO

      1. Of course !

        in my custom log of my workspace log, i have this list

        Custom Logs
        SU_AvailableUpdates_CL
        SU_CompatMarkers_CL
        SU_DeviceInfo_CL
        SU_MDMUpdatePolicy_CL
        SU_OSLatestUpdates_CL
        SU_OSSupportMatrix_CL
        SU_OSUpdateHistory_CL
        SU_UpdateLog_CL
        SU_WindowsSetup_CL
        SU_WindowsSetupErrorCodes_CL
        SU_WUClientInfo_CL
        SU_WUErrorCodes_CL
        SU_WUPolicySettings_CL
        SU_WUPolicyState_CL

        but when i run the runbook, i have this error message:

        #endregion : Invocation of the Log Analytics query failed again: Operation returned an invalid status code ‘BadRequest’
        + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
        + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException

        Invoke-AzOperationalInsightsQuery : Operation returned an invalid status code ‘BadRequest’
        At line:1244 char:19
        + … $Result = Invoke-AzOperationalInsightsQuery -Workspace $Workspace – …
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : CloseError: (:) [Invoke-AzOperationalInsightsQuery], ErrorResponseException
        + FullyQualifiedErrorId : Microsoft.Azure.Commands.OperationalInsights.Query.InvokeOperationalInsightsQuery

  2. Hello,

    i have successfully deploy your solution but in PowerBI, i have not all device in compliance status

    just 160 devices

    for information, i have deployed successfuly the proactive remediation script on all 2000 devices

    is it possible to check manually in log analytics ?

    Thx in advance for your help !

      1. i have deploy your Windows Update for Business Custom Reporting from your blog

        all is ok, i have deploy the remediation script
        runbook are ok
        but your PowerBi report i have only 160 devices appear, but i have deploy on all devices 2000 devices

        i don’t know why i do not see all my devices in power bi report

      2. Yes !

        with this query:
        SU_ClientComplianceStatus_CL
        | summarize arg_max(SummarizationTime_t,*) by ComputerName_s

        i see all my devices 1669 Devices

        But with you query from your powerbi i see only 160 devices 😦

      3. This query

        = let Source = Json.Document(Web.Contents(“https://api.loganalytics.io/v1/workspaces/”&#”Workspace ID”&”/query”,
        [Query=[#”query”=”SU_ClientComplianceStatus_CL
        | where isnotnull(SummarizationTime_t)
        | where DisplayVersion_s != “”Dev””
        | summarize arg_max(SummarizationTime_t,*) by IntuneDeviceID_g
        | project
        SummarizationTime_t,
        IntuneDeviceID_g,
        LatestRegularUpdateStatus=LatestRegularUpdateStatus_s,
        LatestRegularComplianceState=ComplianceStateRegular_s,
        LatestRegularUpdateName=LatestRegularUpdateName_s,
        LatestPreviewComplianceState=ComplianceStatePreview_s,
        LatestPreviewUpdateName=LatestPreviewUpdateName_s,
        LatestOutofBandComplianceState=ComplianceStateOutofBand_s,
        LatestOutofBandUpdateName=LatestOutofBandUpdateName_s,
        LatestRegularLess1ComplianceState=ComplianceStateRegularLess1_s,
        LatestRegularUpdateLess1Name=LatestRegularUpdateLess1Name_s,
        LatestPreviewLess1ComplianceState=ComplianceStatePreviewLess1_s,
        LatestPreviewUpdateLess1Name=LatestPreviewUpdateLess1Name_s,
        LatestOutofBandLess1ComplianceState=ComplianceStateOutofBandLess1_s,
        LatestOutofBandUpdateLess1Name=LatestOutofBandUpdateLess1Name_s,
        LatestRegularLess2ComplianceState=ComplianceStateRegularLess2_s,
        LatestRegularUpdateLess2Name=LatestRegularUpdateLess2Name_s,
        LatestPreviewLess2ComplianceState=ComplianceStatePreviewLess2_s,
        LatestPreviewUpdateLess2Name=LatestPreviewUpdateLess2Name_s,
        LatestOutofBandLess2ComplianceState=ComplianceStateOutofBandLess2_s,
        LatestOutofBandUpdateLess2Name=LatestOutofBandUpdateLess2Name_s,
        Windows_Release=Windows_Release_s,
        OSVersion=OSVersion_s,
        OSVersionSupportStatus=OSVersionSupportStatus_s,
        LastSyncTime=LastSyncTime_t
        | summarize
        Count=dcount(IntuneDeviceID_g,4) by SummarizationTime_t,
        LatestRegularComplianceState,
        LatestRegularUpdateStatus,
        LatestRegularUpdateName,
        LatestPreviewComplianceState,
        LatestPreviewUpdateName,
        LatestOutofBandComplianceState,
        LatestOutofBandUpdateName,
        LatestRegularLess1ComplianceState,
        LatestRegularUpdateLess1Name,
        LatestPreviewLess1ComplianceState,
        LatestPreviewUpdateLess1Name,
        LatestOutofBandLess1ComplianceState,
        LatestOutofBandUpdateLess1Name,
        LatestRegularLess2ComplianceState,
        LatestRegularUpdateLess2Name,
        LatestPreviewLess2ComplianceState,
        LatestPreviewUpdateLess2Name,
        LatestOutofBandLess2ComplianceState,
        LatestOutofBandUpdateLess2Name,
        Windows_Release,
        OSVersion,
        OSVersionSupportStatus,
        bin(LastSyncTime,1d)
        | order by SummarizationTime_t desc
        “,#”x-ms-app”=”OmsAnalyticsPBI”,#”timespan”=”P”&#”Data retention period”&”D”,#”prefer”=”ai.response-thinning=true”],Timeout=#duration(0,0,4,0)])),
        TypeMap = #table(
        { “AnalyticsTypes”, “Type” },
        {
        { “string”, Text.Type },
        { “int”, Int32.Type },
        { “long”, Int64.Type },
        { “real”, Double.Type },
        { “timespan”, Duration.Type },
        { “datetime”, DateTimeZone.Type },
        { “bool”, Logical.Type },
        { “guid”, Text.Type },
        { “dynamic”, Text.Type }
        }),
        DataTable = Source[tables]{0},
        Columns = Table.FromRecords(DataTable[columns]),
        ColumnsWithType = Table.Join(Columns, {“type”}, TypeMap , {“AnalyticsTypes”}),
        Rows = Table.FromRows(DataTable[rows], Columns[name]),
        Table = Table.TransformColumnTypes(Rows, Table.ToList(ColumnsWithType, (c) => { c{0}, c{3}}))
        in
        Table

  3. i think i have found an issue, Many devices are the column “LastsyncTime_t” = null
    i don’t know why 😦

    1. Ok, I can confirm the query is still good. Make sure the data retention period you set in the report variables is sufficient to retrieve enough historic data. In your case, you could check that the Intune LastSyncTime is being successfully retrieved by the Remediation script – the code for that begins on line 450.

      1. i have try this on my computer from your remediation script

        try
        {
        # This one seems the most accurate
        $LastSyncTime = Get-WinEvent -FilterHashtable @{
        LogName=’Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Operational’
        ProviderName=’Microsoft-Windows-DeviceManagement-Pushrouter’
        Id = 300
        } -ErrorAction Stop |
        Where {$_.Message -match “The operation completed successfully”} |
        Select -First 1 -ExpandProperty TimeCreated |
        Get-Date -Format “s” -ErrorAction SilentlyContinue
        }
        catch
        {
        $_.Exception.Message
        try
        {
        # Fallback to this, not as accurate though
        $LastSyncTime = Get-WinEvent -FilterHashtable @{
        LogName=’Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin’
        ProviderName=’Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider’
        Id = 209
        } -ErrorAction Stop |
        Where {$_.Message -match “The operation completed successfully”} |
        Select -First 1 -ExpandProperty TimeCreated |
        Get-Date -Format “s” -ErrorAction SilentlyContinue
        }
        catch
        {
        $LastSyncTime = $null
        $_.Exception.Message
        }
        }

        and the result is good on my computer
        PS C:\Users\rherrero-prest> $LastSyncTime
        2023-07-22T07:53:28

        But noting in log analytics 😦

        only 160 device with this data

        i don’t know why – strange :-/

  4. hi @rherreroprestredoutefr 
    were you able to find the solution for this issue?
    I have the similar issue,
    when running for the first time it does not finish with error:
    Invocation of the Log Analytics query failed again: Operation returned an invalid status code ‘BadRequest’
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    Tried after few hours but still no go
    So I have output
    Output: 200 x 2
    then error about PArseTIme and then
    “Let’s try the LA query again…”
    “Invocation of the Log Analytics query failed again:

Leave a reply to rherreroprestredoutefr Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.