Home About Tools Projects Guides & Blog ⚡ Hire Me ✦ Websites Contact →
Home Projects Building a Monthly M365 Security Report with PowerShell
PowerShell · Graph API · Reporting

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.

Status Complete
Language PowerShell 7
API Microsoft Graph v1.0
Schedule Monthly, 1st at 08:00
Licence M365 Business Premium / E3
Category Automation · Reporting
// overview

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.

ℹ️
Licence note: Risky sign-in data via the Identity Risk APIs requires Entra ID P2 or M365 Business Premium. Secure Score and MFA registration data are available on lower licence tiers. If you're on E3 without P2, skip the risky sign-in section or use Conditional Access sign-in logs as a proxy.
// data sources

What the script pulls

Secure ScoreGRAPH
Current Secure Score as a percentage and point value, plus the 30-day history so the report shows whether the score went up or down since last month. Endpoint: /security/secureScores
Risky Sign-insGRAPH P2
Count of medium and high risk sign-ins in the past 30 days, broken down by risk level. Includes the top five affected users. Endpoint: /identityProtection/riskyUsers
MFA RegistrationGRAPH
Count of users registered for MFA vs total licensed users, shown as a percentage. Highlights any accounts with no MFA method registered. Endpoint: /reports/credentialUserRegistrationDetails
Non-Compliant DevicesGRAPH
Count of Intune-managed devices not in a compliant state at the time the report runs. Same query as the daily report but included here for the monthly snapshot. Endpoint: /deviceManagement/managedDevices
// the script

Key script sections

Authentication uses the same client credentials app registration pattern as the other Graph API projects. Below are the key data-pull sections.

PowerShell - Secure Score pull
# 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" }
PowerShell - MFA registration gap
# 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)
PowerShell - Build and send HTML email
# 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
// outcomes

What this delivers

4
Key security metrics tracked each month automatically
0
Minutes of manual effort after initial setup
Management have monthly security visibility without Intune access
£0
Additional cost over existing M365 licences
Extend it easily. Once the auth pattern is in place, adding new data sources is just adding a new Graph API call and a new table row. Common additions include Azure AD app registration count, admin role holders list, and guest user count.