To create a Microsoft 365 security report in Word, read the figures with Microsoft Graph PowerShell and write them into a document through Word, or build a report template once and have Altivane write a branded document for each tenant. The script gives a plain document and needs Word installed. Figures can lag by a day or more.
| Method | Best fit |
|---|---|
| PowerShell | A plain Word document of a few figures, written on a computer that has Word. |
| Admin centre | A look at a figure on screen, or a usage report exported to CSV. There is no single document. |
| Altivane | Recommended for branded Word reports, 1 for each tenant, from a template you build once. |
Before you start
Use an account that can read all 3 sources: Global Reader covers Secure Score, the MFA registration report and the licence list. The registration report needs Microsoft Entra ID P1 or P2. The script requests SecurityEvents., AuditLog. and LicenseAssignment., which may need administrator consent, and it needs Word on a Windows computer.
Secure Score can take 24 to 48 hours to show a change, and the registration report up to 36 hours; it leaves out disabled accounts. The examples read the tenant and write 1 file on your computer; they change nothing in the tenant.
Option 1
Create a security report in Word with PowerShell
Install the Microsoft Graph modules once; Word needs none. The script reads the latest Secure Score, MFA registration and licences, then saves them as security-report. in the folder you run it from.
# Install once, if the modules are not already installed.
$modules = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Security',
'Microsoft.Graph.Reports', 'Microsoft.Graph.Identity.DirectoryManagement'
Install-Module $modules -Scope CurrentUser$ErrorActionPreference = 'Stop'
# Word's built-in styles are numbers: -63 is Title, -2 is Heading 1 and -1 is Normal.
$styleTitle = -63
$styleHeading = -2
$styleNormal = -1
function Add-Paragraph($Selection, [string]$Text, [int]$Style) {
$Selection.Style = $Style
$Selection.TypeText($Text)
$Selection.TypeParagraph()
}
# Writes objects as a table with a bold header row, then moves to the end of the document.
function Add-Table($Doc, $Selection, $Objects, [string[]]$Columns) {
$table = $Doc.Tables.Add($Selection.Range, $Objects.Count + 1, $Columns.Count)
$table.Borders.Enable = $true
for ($c = 0; $c -lt $Columns.Count; $c++) {
$table.Cell(1, $c + 1).Range.Text = $Columns[$c]
}
for ($r = 0; $r -lt $Objects.Count; $r++) {
for ($c = 0; $c -lt $Columns.Count; $c++) {
$table.Cell($r + 2, $c + 1).Range.Text = [string]$Objects[$r].($Columns[$c])
}
}
$table.Rows.Item(1).Range.Font.Bold = 1
$null = $Selection.EndKey(6)
}
$scopes = 'SecurityEvents.Read.All', 'AuditLog.Read.All', 'LicenseAssignment.Read.All'
Connect-MgGraph -Scopes $scopes -NoWelcome
$word = $null
$doc = $null
try {
# Read the figures from Microsoft Graph.
$score = Get-MgSecuritySecureScore -Top 1
$members = @(Get-MgReportAuthenticationMethodUserRegistrationDetail -All |
Where-Object { $_.UserType -eq 'member' })
$skus = @(Get-MgSubscribedSku -All | Where-Object { $_.AppliesTo -eq 'User' } |
Sort-Object SkuPartNumber)
$scoreText = 'Microsoft has not worked out a Secure Score for this tenant yet.'
$worked = 'not available'
if ($score -and $score.MaxScore -gt 0) {
$percent = [math]::Round(100 * $score.CurrentScore / $score.MaxScore, 1)
$scoreText = "$($score.CurrentScore) of $($score.MaxScore) ($percent%)"
$worked = $score.CreatedDateTime.ToUniversalTime().ToString('d MMM yyyy') + ' UTC'
}
$mfaText = 'No registration details were returned.'
$adminsWithout = 0
if ($members.Count -gt 0) {
$registered = @($members | Where-Object { $_.IsMfaRegistered }).Count
$share = [math]::Round(100 * $registered / $members.Count, 1)
$mfaText = "$registered of $($members.Count) enabled members ($share%)"
$open = @($members | Where-Object { $_.IsAdmin -and -not $_.IsMfaRegistered })
$adminsWithout = $open.Count
}
$facts = @(
[pscustomobject]@{ Figure = 'Secure Score'; Value = $scoreText },
[pscustomobject]@{ Figure = 'Registered for MFA'; Value = $mfaText },
[pscustomobject]@{ Figure = 'Administrators with no MFA method'; Value = $adminsWithout }
)
$licences = @(foreach ($sku in $skus) {
$bought = $sku.PrepaidUnits.Enabled
[pscustomobject]@{
Subscription = $sku.SkuPartNumber
Bought = $bought
Assigned = $sku.ConsumedUnits
Unassigned = $bought - $sku.ConsumedUnits
}
})
# Write them into a new Word document and save it.
$today = Get-Date -Format 'd MMMM yyyy'
$path = Join-Path $PWD.Path 'security-report.docx'
$word = New-Object -ComObject Word.Application
$doc = $word.Documents.Add()
$selection = $word.Selection
Add-Paragraph $selection 'Microsoft 365 security report' $styleTitle
Add-Paragraph $selection "Figures read from Microsoft Graph on $today." $styleNormal
Add-Paragraph $selection 'Headline figures' $styleHeading
Add-Table $doc $selection $facts 'Figure', 'Value'
Add-Paragraph $selection "Secure Score was worked out on $worked." $styleNormal
if ($licences.Count -gt 0) {
Add-Paragraph $selection 'Licences' $styleHeading
Add-Table $doc $selection $licences 'Subscription', 'Bought', 'Assigned', 'Unassigned'
}
$doc.SaveAs2($path, 12) # 12 is wdFormatXMLDocument, the .docx format.
Write-Output "Saved $path"
} finally {
Disconnect-MgGraph | Out-Null
if ($doc) { $doc.Close(0) }
if ($word) { $word.Quit() }
}The result is a plain document in Word's built-in styles with 2 tables. A cover page, logo, colours, contents, charts and a rating against limits are each more script, tested against your version of Word. Word replaces a file of the same name without asking, and Microsoft does not support running its automation unattended, such as from a service.
Option 2
Gather security figures in the admin centres
- Sign in to the Microsoft 365 admin centre. Billing > Licenses shows the licences assigned and unassigned. Reports > Usage opens the usage reports, and Export on one downloads its data as a CSV file.
- Open Secure Score in the Microsoft Defender portal. It shows your score, and the History tab shows it over time.
- In the Microsoft Entra admin centre, go to Entra ID > Authentication methods > Activity. The Registration tab shows how many people are capable of multifactor authentication.
- Copy the figures into a Word document you lay out yourself.
Usage reports show activity, not security, and hide user names by default. None of these pages produces 1 document with your branding, so every report is assembled manually. That suits a figure for a meeting, not a monthly report for several clients.
Option 3 ยท Recommended for branded reports for each tenant
Create a security report in Word with Altivane
- Open Altivane and choose the tenants to build. They run 1 after another. Each has its own report template, client name and client logo, or follows the default template.
- Choose the report template, or build one. Start from Standard, the full review, or Blank. Then set the brand (company name and logo, an accent colour or a ready-made look, fonts, a footer line) and the blocks it holds, such as the cover page, charts and each scorecard report's summary.
- Build the documents. Altivane runs the scorecard reports that are not off for each tenant, read only, and writes a Word report from the template and an Excel workbook of every table.
- Open each tenant's Word report. Key figures gives Secure Score, staff registered for MFA, devices compliant and the critical findings, and Findings lists what was rated Critical or Warning. The workbook has the Scorecard and every row.
Each client gets the same look with their own name and logo, and the count in each table is rated against limits you can change. A figure that could not be read shows as n/a, not 0. AI summaries are optional: they stay off until AI is set up and switched on for the tenant, and only report headings, status counts and summary figures are sent, never names or detail tables.
Contact usCommon questions
How do I add my logo and colours?
In Altivane, set the template's brand: company name and logo, an accent colour or a ready-made look, fonts and a footer line. The client's name and logo are set for each tenant. In a script, each is more code.
How are the figures checked against limits?
A table with a limit is rated from its count, or its share of a base such as all enabled members. Critical is tested first, and a table that could not be read is Not collected. By default, enabled users with no MFA registration are Warning at 2% and Critical at 10%. The Word report lists the Critical and Warning tables as findings, and the workbook has the whole scorecard.
Why does my report not show what I changed today?
Secure Score and the registration report update slowly, so a report built before they do shows the earlier figures. For what moves the score, see how to check Secure Score in Microsoft Defender.
Microsoft references
Technical references checked on 20 October 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant or a copy of Word.
- Microsoft Secure Score
- Get-MgSecuritySecureScore reference
- Get-
Mg Report Authentication Method User Registration Detail reference - Get-MgSubscribedSku reference
- Authentication methods activity in Microsoft Entra ID
- Usage reports in the Microsoft 365 admin centre
- Documents.Add method (Word)
- Tables.Add method (Word)
- Document.SaveAs2 method (Word)
- Considerations for unattended automation of Office