Tracking Windows 11 Upgrades with Azure Automation and Intune

In today’s blog, I will address a question from one of our community members, who is looking to create a report for tracking Windows 11 upgrades via Azure Automation Runbook and Microsoft Intune. He has tried to gather enrolled devices details using a runbook but hasn’t found a solution yet. In this post, we will demonstrate how to generate a report on Windows 11 upgrade tracking with Intune and Azure Automation.

Content

  1. Content
  2. How to create an azure automation account
  3. How to get the report

How to create an azure automation account

I have already written a very detailed blog on how to get started with Azure Automamtion and Runbooks. It’s best to read this blog first. You have to make sure that the Graph.Authentication module is installed and you need API graph permissions for:

  • Mail.Send
  • DeviceManagementManagedDevices.Read.All

How to get the report

You can find the script in my GitHub repository or here. You have to copy the script into you runbook:

# Variables
$MailTo = ""
$MailSender = ""

# Authenticate and connect to Microsoft Graph
Connect-AzAccount -Identity
$token = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com"

#Connect to Microsoft Graph API
Connect-MgGraph -AccessToken $token.Token

# Get Windows devices from Intune
$devices = Get-MgDeviceManagementManagedDevice -all -Filter "contains(operatingSystem,'Windows')"


# Filter Windows 11 devices
$windows11Devices = $devices | Where-Object { $_.OsVersion -ge "10.0.22000" }

# Calculate device counts
$totalDevices = $devices.Count
$windows11DevicesCount = $windows11Devices.Count

# Calculate pie chart percentages
$windows11Percentage = ($windows11DevicesCount / $totalDevices) * 100
$otherWindowsPercentage = 100 - $windows11Percentage


# Create HTML report
$html = @"
<style>
    body {
        font-family: Arial, sans-serif;
    }
    h1 {
        font-size: 28px;
        color: #0078d4;
        margin-top: 0;
        text-align: center;
    }
    .chart-container {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
    }
    .pie-chart {
        width: 300px;
        height: 300px;
        margin: 20px;
    }
    .device-list {
        margin-top: 40px;
    }
    table {
        border-collapse: collapse;
        width: 100%;
    }
    th {
        background-color: #0078d4;
        color: #fff;
        font-weight: bold;
        padding: 8px;
        text-align: left;
    }
    td {
        border: 1px solid #ddd;
        padding: 8px;
    }
    tr:nth-child(even) {
        background-color: #f2f2f2;
    }
</style>
<h1>Windows 11 Adoption Report</h1>
<div class="chart-container">
    <div class="pie-chart">
        <canvas id="chart"></canvas>
    </div>
</div>
<div class="device-list">
    <table>
        <thead>
            <tr>
                <th>Device Name</th>
                <th>User</th>
                <th>Last Sync DateTime</th>
                <th>OSVersion</th>
            </tr>
        </thead>
        <tbody>
"@
foreach ($device in $devices) {
    $html += "<tr><td>$($device.DeviceName)</td><td>$($device.EmailAddress)</td><td>$($device.LastSyncDateTime)</td><td>$($device.OSVersion)</td></tr>"
}
$html += @"
        </tbody>
    </table>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
    var ctx = document.getElementById('chart').getContext('2d');
    var chart = new Chart(ctx, {
        type: 'pie',
    data: {
            labels: ['Windows 11', 'Other Windows'],
            datasets: [{
            backgroundColor: ['#0078d4', '#f2f2f2'],
            data: [$windows11Percentage, $otherWindowsPercentage]
            }]
            },
            options: {
            legend: {
            display: true,
            position: 'bottom',
            labels: {
            fontColor: '#333',
            fontSize: 14,
            padding: 16
            }
            }
            }
            });
            </script>
"@

$html | Out-File -FilePath "Windows11AdoptionReport.html"
$base64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes(".\Windows11AdoptionReport.html"))

#Send Mail    
$URLsend = "https://graph.microsoft.com/v1.0/users/$MailSender/sendMail"
$BodyJsonsend = @"
{
    "message": {
      "subject": "Intune error report",
      "body": {
        "contentType": "Text",
        "content": "Dear Admin, this Mail contains the Windows 11 Adoption report"
      },
      "toRecipients": [
        {
          "emailAddress": {
            "address": "$MailTo"
          }
        }
      ],
      "attachments": [
        {
          "@odata.type": "#microsoft.graph.fileAttachment",
          "name": "Windows11AdoptionReport.html",
          "contentType": "text/plain",
          "contentBytes": "$base64"
        }
      ]
    }
  }
"@

$response = Invoke-MgRestMethod -Method POST -Uri $URLsend -Body $BodyJsonsend

You have to add the sender and receiver email into this variable:

$MailTo = ""
$MailSender = ""

12 thoughts on “Tracking Windows 11 Upgrades with Azure Automation and Intune

  1. Hey Jannik, I have been following the steps outlined in this however I seem to be running into an issue with the access token explaining you must specify access token, any ideas please?

    Like

    • Hey Jannik, I managed to work around it via a similar way but appreciate this. I also come into the following issue which I didn’t manage to bug System.Management.Automation.MethodInvocationException: Exception calling “ReadAllBytes” with “1” argument(s): “Could not find file ‘C:\Windows\System32\Windows11AdoptionReport.html’.” —> System.IO.FileNotFoundException: Could not find file ‘C:\Windows\System32\Windows11AdoptionReport.html’. It doesn’t seem to Output the file so cant find it.

      Like

  2. The script works fine at outputting the file to the windows\system32 folder when ran locally however seems to be having issue in the runbook

    Like

  3. Hey Jannik, I’ve got the same problem as Jack with the file not writing out. I also have an issue that my registered application can’t send mail as the user. How did you manage the credentials/permissions for this in your environment?

    Like

    • Hi Kristen, I managed the credentials for the application by following Janniks guide using the below

      Install-Module Microsoft.Graph -Scope CurrentUser

      Connect-MgGraph -Scopes Application.Read.All, AppRoleAssignment.ReadWrite.All, RoleManagement.ReadWrite.Directory

      $managedIdentityId = “Managed Identity Object ID”
      $roleName = “DeviceManagementApps.Read.All”

      $msgraph = Get-MgServicePrincipal -Filter “AppId eq ‘00000003-0000-0000-c000-000000000000′”
      $role = $Msgraph.AppRoles| Where-Object {$_.Value -eq $roleName}

      New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $managedIdentityId -PrincipalId $managedIdentityId -ResourceId $msgraph.Id -AppRoleId $role.Id

      Disconnect-MgGraph

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s