Powershell : Intune Laptop Compliance Report (Grace Peroid)

How can you track when a Intune managed laptop will “expire” due to not being used? Well this was another requirement that draws on other blog posts about API and Microsoft Graph calls and this time it focuses on the Intune device cleanup policy which you can find from the following navigation:

Intune Admin Centre > Devices >  Device Cleanup Rules as below:

This policy will mean that if you have laptops that have not "checked in " in 90 days then they will be deleted which could cause a problem for the users of those laptops, so this report will email you laptops that have reached 60 days (if you are like me then not using your laptop for 60 days is impossible).

If you fail this check then your device will be deleted and your laptop will cease to function with a managed account you will need to contact your admin.

You can export this data and complete these actions manually but scripting is so much cooler and manually downloading the CSV and analyzing it yourself as it’s automated and consistent.

Pre-Flight : Create the App Registration

First we need to create the App Registration so we need to head over to Entra:



Then we need App Registrations


Then we need a new registration as below:




Then we need to give it a valid name and ensure its single tenant then Register this application:


From here we need Certificates and Secrets as below:



Then ensure you have "client secrets" selected and choose "New client secret"



You then need to give that secret a name and lifetime:


You will then see this secret below:

Note : You will only see the value once when you navigate away from this screen and back the value will no longer be visible!


Next we need API permissions:


Then you need an API permission, for this we need Microsoft Graph:


Then you need an application permission:


Now we need to find the permissions so when you get the search option enter DeviceManagementManagedDevices.Read.All and choose that option and Add that permission as below:



When you add these permission you should see then as valid API permissions as below:



You then need to grant admin consent for these permissions:


Which you will need to confirm:


Then you will see that the "granted consent" is now approved with the green ticks:

You will also need to know the tenant ID and application ID which you can get from the overview section of the App Registration:



You will need the following information for this script to work so keep it handy:

Tenant ID
Application ID
Secret Key

Now we have the App Registration done we can now move on to the scripting to get the data from Intune and export it to a CSV file so we can then analyse that file and create the visuals, the end result will look something like this:

The Final Visual Report

Note that the laptops at this stage have not expired but this is early warning for the expiry, that is set to 90 days however if you set this report to 90 days then its game over by the time you read the report - this way you get 30 days or "prevention" before the purge.



Collect the CSV report from the API

First we need the script to extract the data from the API, you need to update the variables in bold as collected earlier.

Script : ComplianceReport.ps1

# PowerShell script to export Intune-managed Windows devices to CSV
# Required modules: Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement

# App registration details
$tenantId = "<tenant_id)"
$clientId = "<client_id>"
$clientSecret = "<secreet_value>"

# Authentication
$body = @{
    grant_type    = "client_credentials"
    client_id     = $clientId
    client_secret = $clientSecret
    scope         = "https://graph.microsoft.com/.default"
}

$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$tokenResponse = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body
$accessToken = $tokenResponse.access_token

# Headers for Graph API calls
$headers = @{
    "Authorization" = "Bearer $accessToken"
    "Content-Type"  = "application/json"
}

# Get managed devices
$deviceUrl = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices"
$devices = @()
$nextLink = $deviceUrl

do {
    $response = Invoke-RestMethod -Uri $nextLink -Headers $headers -Method Get
    
    # Filter for Windows devices managed by Intune MDM
    $filteredDevices = $response.value | Where-Object {
        $_.operatingSystem -eq "Windows" -and 
        $_.managementAgent -eq "MDM"
    }
    
    $devices += $filteredDevices
    $nextLink = $response.'@odata.nextLink'
} while ($nextLink)

# Select relevant properties
$deviceDetails = $devices | Select-Object -Property `
    deviceName,
    serialNumber,
    operatingSystem,
    osVersion,
    lastSyncDateTime,
    enrolledDateTime,
    managementState,
    complianceState,
    manufacturer,
    model,
    userPrincipalName,
    emailAddress,
    azureADDeviceId,
    deviceEnrollmentType,
    ownership,
    freeStorageSpaceInBytes,
    totalStorageSpaceInBytes

# Export to CSV
$exportPath = "IntuneDevices_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$deviceDetails | Export-Csv -Path $exportPath -NoTypeInformation

Write-Host "Exported $($deviceDetails.Count) devices to $exportPath"

# Display summary
Write-Host "`nDevice Summary:"
Write-Host "Total Windows devices managed by Intune MDM: $($deviceDetails.Count)"
Write-Host "Compliant devices: $(($deviceDetails | Where-Object { $_.complianceState -eq 'Compliant' }).Count)"
Write-Host "Non-compliant devices: $(($deviceDetails | Where-Object { $_.complianceState -eq 'NonCompliant' }).Count)"

This will give you the file highlighted below as a CSV file, this will contain all your MDM managed Windows laptops and not the mobile phones or other peripheral devices - as they are not relevant here:


Produce the report in HTML

How we have the report in CSV format we now need to produce the report, this report will look for the latest CSV file in the current folder and apply its formatting to that file, as if my magic.

Script : LaptopReporter.ps1

# Get the latest CSV file in the current directory
$latestCsv = Get-ChildItem -Filter *.csv | Sort-Object LastWriteTime -Descending | Select-Object -First 1

# Read the CSV content
$csvData = Import-Csv $latestCsv.FullName

# Initialize counters
$totalLaptops = $csvData.Count
$expiredLaptops = @()
$validLaptops = 0

# Process each row
foreach ($row in $csvData) {
    # Handle blank userPrincipalName
    if ([string]::IsNullOrWhiteSpace($row.userPrincipalName)) {
        $row.userPrincipalName = "Unassigned"
    }

    # Check last sync date
    try {
        $lastCheckIn = [datetime]::Parse($row.lastSyncDateTime)
        $daysSinceCheckIn = (Get-Date) - $lastCheckIn
        if ($daysSinceCheckIn.Days -gt 60) {
            $expiredLaptops += @{
                DeviceName = $row.deviceName
                UserUPN = $row.userPrincipalName
                LastCheckIn = $lastCheckIn.ToString("yyyy-MM-dd")
                DaysSinceCheckIn = $daysSinceCheckIn.Days
            }
        } else {
            $validLaptops++
        }
    }
    catch {
        Write-Warning "Failed to parse date for device $($row.deviceName): $($row.lastSyncDateTime)"
        # Count as expired if date parsing fails
        $expiredLaptops += @{
            DeviceName = $row.deviceName
            UserUPN = $row.userPrincipalName
            LastCheckIn = "Unknown"
            DaysSinceCheckIn = "Unknown"
        }
    }
}

# Calculate percentages
$validPercentage = [math]::Round(($validLaptops / $totalLaptops) * 100, 2)
$expiredPercentage = [math]::Round(($expiredLaptops.Count / $totalLaptops) * 100, 2)

# Generate HTML report
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Laptop Status Report</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
            color: #333;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .header {
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 2px solid #eee;
        }
        .summary {
            display: flex;
            justify-content: space-around;
            margin-bottom: 30px;
        }
        .stat-box {
            text-align: center;
            padding: 20px;
            border-radius: 8px;
            width: 45%;
        }
        .healthy {
            background-color: #e8f5e9;
            color: #2e7d32;
        }
        .expired {
            background-color: #ffebee;
            color: #c62828;
        }
        .percentage {
            font-size: 36px;
            font-weight: bold;
            margin: 10px 0;
        }
        .progress-bar-container {
            width: 100%;
            height: 20px;
            background-color: rgba(0, 0, 0, 0.1);
            border-radius: 10px;
            overflow: hidden;
            margin: 10px 0;
        }
        .progress-bar {
            height: 100%;
            border-radius: 10px;
            transition: width 1s ease-in-out;
        }
        .progress-healthy {
            background-color: #4caf50;
        }
        .progress-expired {
            background-color: #e53935;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        th {
            background-color: #f8f9fa;
            font-weight: 600;
        }
        tr:hover {
            background-color: #f5f5f5;
        }
        .timestamp {
            text-align: center;
            color: #666;
            margin-top: 20px;
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Laptop Status Report</h1>
            <p>Generated on $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</p>
        </div>
                <div class="summary">
            <div class="stat-box healthy">
                <h2>Healthy Laptops</h2>
                <div class="percentage">$validPercentage%</div>
                <div class="progress-bar-container">
                    <div class="progress-bar progress-healthy" style="width: $validPercentage%;"></div>
                </div>
                <p>$validLaptops out of $totalLaptops laptops</p>
            </div>
                       <div class="stat-box expired">
                <h2>Expired Laptops</h2>
                <div class="percentage">$expiredPercentage%</div>
                <div class="progress-bar-container">
                    <div class="progress-bar progress-expired" style="width: $expiredPercentage%;"></div>
                </div>
                <p>$($expiredLaptops.Count) out of $totalLaptops laptops</p>
            </div>
        </div>
        $(if ($expiredLaptops.Count -gt 0) {
        @"
        <h2>Expired Laptops Details</h2>
        <table>
            <thead>
                <tr>
                    <th>Device Name</th>
                    <th>Primary User UPN</th>
                    <th>Last Check-in</th>
                    <th>Days Since Check-in</th>
                </tr>
            </thead>
            <tbody>
                $(foreach ($laptop in $expiredLaptops) {
                    "<tr>
                        <td>$($laptop.DeviceName)</td>
                        <td>$($laptop.UserUPN)</td>
                        <td>$($laptop.LastCheckIn)</td>
                        <td>$($laptop.DaysSinceCheckIn)</td>
                    </tr>"
                })
            </tbody>
        </table>
"@
        })
                <div class="timestamp">
            <p>Report based on file: $($latestCsv.Name)</p>
        </div>
    </div>
</body>
</html>
"@

# Save the HTML report
$reportPath = "LaptopReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
$html | Out-File -FilePath $reportPath -Encoding UTF8

This will then produce that report which was illustrated earlier.

Previous Post Next Post

نموذج الاتصال