Monthly M365
Security Report Script
A PowerShell script that queries the Graph API for Secure Score, risky sign-ins, MFA registration gaps, and non-compliant devices, then builds a formatted HTML report and emails it to management on the first of each month.
Why a monthly report
The daily compliance report (documented separately) keeps the IT team across device state. This monthly report is a different audience: management and stakeholders who need a high-level picture of security posture without logging into Intune or the Defender portal themselves.
The report pulls four key metrics: Secure Score and trend, risky sign-in count for the month, MFA registration coverage, and non-compliant device count. All from the Graph API, formatted into a clean HTML email.
What the script pulls
/security/secureScores/identityProtection/riskyUsers/reports/credentialUserRegistrationDetails/deviceManagement/managedDevicesKey script sections
Authentication uses the same client credentials app registration pattern as the other Graph API projects. Below are the key data-pull sections.
# Pull Secure Score (most recent + 30 days history)
$scores = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/security/secureScores?`$top=31" -Headers $h).value
$currentScore = $scores[0].currentScore
$maxScore = $scores[0].maxScore
$lastMonthScore= $scores[-1].currentScore
$scorePct = [math]::Round(($currentScore / $maxScore) * 100, 1)
$trend = $currentScore - $lastMonthScore
$trendStr = if ($trend -ge 0) { "↑ +$trend pts" } else { "↓ $trend pts" }
# MFA registration coverage
$mfaData = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/reports/credentialUserRegistrationDetails" -Headers $h).value
$totalUsers = $mfaData.Count
$mfaRegistered = ($mfaData | Where-Object { $_.isMfaRegistered }).Count
$noMfaUsers = $mfaData | Where-Object { -not $_.isMfaRegistered } | Select-Object -ExpandProperty userPrincipalName
$mfaPct = [math]::Round(($mfaRegistered / $totalUsers) * 100, 1)
# Build the HTML report body
$month = Get-Date -f "MMMM yyyy"
$html = @"
<h2 style='font-family:sans-serif'>M365 Security Report - $month</h2>
<table style='border-collapse:collapse;width:100%;font-family:sans-serif'>
<tr style='background:#1a56db;color:white'><th style='padding:10px'>Metric</th><th style='padding:10px'>Value</th><th style='padding:10px'>Trend / Notes</th></tr>
<tr><td style='padding:10px;border:1px solid #e2e8f0'>Secure Score</td><td style='padding:10px;border:1px solid #e2e8f0'><strong>$scorePct%</strong></td><td style='padding:10px;border:1px solid #e2e8f0'>$trendStr vs last month</td></tr>
<tr><td style='padding:10px;border:1px solid #e2e8f0'>MFA Coverage</td><td style='padding:10px;border:1px solid #e2e8f0'><strong>$mfaPct%</strong></td><td style='padding:10px;border:1px solid #e2e8f0'>$mfaRegistered of $totalUsers users registered</td></tr>
<tr><td style='padding:10px;border:1px solid #e2e8f0'>Non-Compliant Devices</td><td style='padding:10px;border:1px solid #e2e8f0'><strong>$nonCompliant</strong></td><td style='padding:10px;border:1px solid #e2e8f0'>of $totalDevices managed devices</td></tr>
</table>
"@
# Send via Graph API Mail.Send
$mailBody = @{
message = @{
subject = "M365 Security Report - $month"
body = @{ contentType = "HTML"; content = $html }
toRecipients = @(@{ emailAddress = @{ address = $env:REPORT_RECIPIENT } })
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/users/$($env:REPORT_SENDER)/sendMail" -Headers $h -Body $mailBody