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:
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
Then we need App Registrations
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 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:
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:
Tenant ID
Collect the CSV report from the API
Script : ComplianceReport.ps1
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.