Microsoft 365 reporting

How to create a Microsoft 365 security report in Word

Write Secure Score, MFA and licence figures into Word with Microsoft Graph PowerShell, gather them in the admin centres, or build a branded Word report for each tenant with Altivane.

By the Published

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.

Choose the method that fits the job
MethodBest fit
PowerShellA plain Word document of a few figures, written on a computer that has Word.
Admin centreA look at a figure on screen, or a usage report exported to CSV. There is no single document.
AltivaneRecommended 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.Read.All, AuditLog.Read.All and LicenseAssignment.Read.All, 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.docx in the folder you run it from.

PowerShell
# 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
PowerShell
$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

  1. 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.
  2. Open Secure Score in the Microsoft Defender portal. It shows your score, and the History tab shows it over time.
  3. 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.
  4. 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.

Common 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.

Talk to us about Altivane

Questions, or pricing? Get in touch.

Contact us