Support desk — Monday to Friday, 08:30 to 17:30Client AreaGet help now →
Free tools for your business

Microsoft 365 security check

Run one read only script against your own tenant and see what it says about multifactor coverage, legacy authentication, admin accounts and guest access. Nothing is installed, nothing is changed, and no data leaves your machine.

Read only, changes nothingNo consent grant to usResults stay in your browser
Read the script before you run it. It is printed in full below, it runs to about a hundred lines, and every one of them is readable. We would rather you checked it than took our word for it. If anything in it looks wrong to you, do not run it.
Before you start, two things to expect

1. It has to download first, and that takes a while. PowerShell does not come with the Microsoft Graph commands, so the script fetches five modules from Microsoft before it can do anything. That is usually under a minute, but on a slow connection it can run to several, and for most of that time the screen will sit there saying nothing. Let it finish. Interrupting it leaves a half installed module that has to be cleared out before it will work.

2. Your browser will open and ask you to sign in. A Microsoft sign in page appears in a popup window, asking you to log in to your Microsoft 365 account and approve read only access for the Microsoft Graph Command Line Tools. That is Microsoft’s own application rather than ours, and the sign in is between you and Microsoft. If the popup is blocked, allow it and run the script again.

How it works

1

Check who is running it

The first person to run this in your organisation has to be a Global Administrator. Microsoft requires an administrator to approve the read only permissions once, before anybody can use them. That approval is a one off, and once it has been given a Global Reader account is enough for every run after it. You do not need local administrator rights on the computer itself.

On Windows, open Windows PowerShell or PowerShell 7. On a Mac you will need PowerShell 7, which is not preinstalled: either run brew install --cask powershell if you use Homebrew, or download the installer from Microsoft, then type pwsh in Terminal to start it. The script behaves the same on both.

2

Run the script

Copy it, paste it into PowerShell and press return.

If you see a screen headed Need admin approval rather than an ordinary sign in, your account is not able to grant that approval. Sign in with a Global Administrator account instead, or ask whoever holds one to run the script this first time. Once it has been approved, that screen will not appear again for anyone.

<# EasyLifeIT  |  Microsoft 365 security check  |  READ ONLY

   What this does
     Signs in to Microsoft Graph using read only permissions, counts a
     handful of configuration items, and prints a short summary as JSON.

   What it does not do
     It changes nothing in your tenant. It collects no names, no email
     addresses and no message content, only counts and settings. The
     summary stays on your machine unless you choose to paste it somewhere.

   Please read this script before you run it. You should never run anything
   from the internet that you have not looked at first. #>

$ErrorActionPreference = 'Stop'

function Invoke-Safe {
    param([scriptblock]$Block, $Default = $null)
    try { & $Block } catch { return $Default }
}

# Only the five Graph modules this script actually uses. Installing the whole
# Microsoft.Graph meta module pulls in about forty and can take fifteen minutes.
$needed = @(
    'Microsoft.Graph.Authentication',
    'Microsoft.Graph.Users',
    'Microsoft.Graph.Identity.DirectoryManagement',
    'Microsoft.Graph.Identity.SignIns',
    'Microsoft.Graph.Reports'
)

foreach ($m in $needed) {
    if (-not (Get-Module -ListAvailable -Name $m)) {
        Write-Host ("Installing {0} for your user account only." -f $m) -ForegroundColor Yellow
        Install-Module $m -Scope CurrentUser -Force -AllowClobber
    }
}

try {
    Connect-MgGraph -NoWelcome -Scopes @(
        'Directory.Read.All',
        'Policy.Read.All',
        'UserAuthenticationMethod.Read.All',
        'AuditLog.Read.All',
        'Reports.Read.All'
    )
} catch {
    Write-Host ''
    Write-Host 'Sign in did not complete.' -ForegroundColor Red
    Write-Host ''
    Write-Host 'If you saw a screen headed "Need admin approval", the read only permissions this'
    Write-Host 'script uses have not been approved in your tenant yet. Microsoft requires an'
    Write-Host 'administrator to approve its own Graph Command Line Tools application once,'
    Write-Host 'before anyone can use it. Ask a Global Administrator to run this script, or to'
    Write-Host 'approve that application, and afterwards a Global Reader account is enough.'
    return
}

$absent = @(
    'Get-MgUser',
    'Get-MgDirectoryRole',
    'Get-MgIdentityConditionalAccessPolicy',
    'Get-MgPolicyAuthorizationPolicy',
    'Get-MgReportAuthenticationMethodUserRegistrationDetail'
) | Where-Object { -not (Get-Command $_ -ErrorAction SilentlyContinue) }

if ($absent) {
    Write-Host ''
    Write-Host 'Some commands are missing, so parts of the check will be skipped:' -ForegroundColor Yellow
    $absent | ForEach-Object { Write-Host ("  {0}" -f $_) -ForegroundColor Yellow }
    Write-Host ''
}

$out = [ordered]@{
    schema    = 1
    generated = (Get-Date).ToUniversalTime().ToString('s') + 'Z'
}

# Accounts
$users = Invoke-Safe { @(Get-MgUser -All -Property Id, AccountEnabled, UserType) } @()
$out.usersTotal   = @($users).Count
$out.usersEnabled = @($users | Where-Object { $_.AccountEnabled }).Count
$out.guests       = @($users | Where-Object { $_.UserType -eq 'Guest' }).Count

# Multifactor authentication registration
$reg = Invoke-Safe { @(Get-MgReportAuthenticationMethodUserRegistrationDetail -All) } $null
if ($null -ne $reg -and @($reg).Count -gt 0) {
    $out.mfaChecked       = $true
    $out.mfaRegistered    = @($reg | Where-Object { $_.IsMfaRegistered }).Count
    $out.mfaNotRegistered = @($reg | Where-Object { -not $_.IsMfaRegistered }).Count
} else {
    $out.mfaChecked = $false
}

# Global administrators
$ga = Invoke-Safe {
    $role = Get-MgDirectoryRole -Filter "RoleTemplateId eq '62e90394-69f5-4237-9190-012177145e10'"
    if ($role) { @(Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All) } else { @() }
} @()
$out.globalAdmins = @($ga).Count

# Security defaults
$sd = Invoke-Safe { Get-MgPolicyIdentitySecurityDefaultEnforcementPolicy } $null
if ($null -ne $sd) { $out.securityDefaults = [bool]$sd.IsEnabled } else { $out.securityDefaults = $null }

# Conditional access
$ca = Invoke-Safe { @(Get-MgIdentityConditionalAccessPolicy -All) } $null
if ($null -ne $ca) {
    $out.caChecked    = $true
    $out.caTotal      = @($ca).Count
    $out.caEnabled    = @($ca | Where-Object { $_.State -eq 'enabled' }).Count
    $out.caReportOnly = @($ca | Where-Object { $_.State -eq 'enabledForReportingButNotEnforced' }).Count
    $out.caBlocksLegacyAuth = @($ca | Where-Object {
            $_.State -eq 'enabled' -and
            $_.GrantControls.BuiltInControls -contains 'block' -and
            ($_.Conditions.ClientAppTypes -contains 'exchangeActiveSync' -or
             $_.Conditions.ClientAppTypes -contains 'other')
        }).Count -gt 0
    $out.caRequiresMfa = @($ca | Where-Object {
            $_.State -eq 'enabled' -and
            $_.GrantControls.BuiltInControls -contains 'mfa' -and
            $_.Conditions.Users.IncludeUsers -contains 'All'
        }).Count -gt 0
} else {
    $out.caChecked = $false
}

# Default user permissions and guest invitations
$auth = Invoke-Safe { Get-MgPolicyAuthorizationPolicy } $null
if ($null -ne $auth) {
    $out.guestInviteSetting   = [string]$auth.AllowInvitesFrom
    $out.usersCanRegisterApps = [bool]$auth.DefaultUserRolePermissions.AllowedToCreateApps
}

# Accounts with no sign in for 90 days (needs Entra ID P1)
$out.staleAccounts = Invoke-Safe {
    $cut = (Get-Date).AddDays(-90)
    @(Get-MgUser -All -Property Id, AccountEnabled, SignInActivity |
        Where-Object {
            $_.AccountEnabled -and
            $_.SignInActivity.LastSignInDateTime -and
            $_.SignInActivity.LastSignInDateTime -lt $cut
        }).Count
} $null

Invoke-Safe { Disconnect-MgGraph | Out-Null } $null

$json = $out | ConvertTo-Json -Depth 4 -Compress
Write-Host ''
Write-Host '----- copy everything between the lines -----' -ForegroundColor Cyan
Write-Host $json
Write-Host '----- end -----' -ForegroundColor Cyan
$copied = Invoke-Safe {
    if ($IsMacOS)   { $json | /usr/bin/pbcopy; $true }
    elseif ($IsLinux) { $false }
    else            { Set-Clipboard -Value $json; $true }
} $false

if ($copied) {
    Write-Host ''
    Write-Host 'Also copied to your clipboard.' -ForegroundColor Green
}
3

Paste the result here

The script prints a single line of JSON and copies it to your clipboard. Paste it below. The analysis runs inside this page, in your browser, and nothing is uploaded.

What each check looks at

Microsoft 365 tenants are rarely compromised through a clever exploit. They are compromised because somebody’s password was stolen and nothing else stood in the way. These are the settings that decide whether that is enough.

Multifactor authentication

The check counts how many accounts have a multifactor method registered, and separately whether anything actually requires one. Those are two different things, and the gap between them is where most businesses sit. A tenant can have multifactor available to everyone and enforced on nobody, which protects the people who opted in and leaves the rest exactly as they were.

Legacy authentication

Older protocols such as IMAP, POP and SMTP basic authentication cannot display a multifactor prompt. If they are still reachable, an attacker holding a valid password can use one of them and never be asked for a second factor. Blocking them is the single highest value change most tenants can make, and it is free.

Global administrators

Every global administrator is a complete compromise of the tenant if that account is taken over, so the number should be small and deliberate. Two or three is usually right for a business of fifty to two hundred people. One is too few, because losing it means nobody can recover the tenant, which is why a second emergency account is standard practice.

Guests and application registration

Guest accounts are added for a project and then outlive it by years. The check counts them and looks at whether any user can invite more. It also reports whether ordinary users are allowed to register applications, which is the foothold consent phishing depends on: a convincing prompt, an approval given in a hurry, and an attacker holding a token that survives a password reset.

Dormant accounts

Accounts that are still enabled but have not signed in for ninety days are a licence you are paying for and a door nobody is watching. They are also the accounts least likely to have multifactor registered, because nobody has been through the prompt.

Security defaults or conditional access

If the check reports that security defaults are switched on, it is worth understanding what that means. Security defaults are Microsoft’s starter settings: they enforce multifactor for everyone and block legacy authentication, which is a reasonable baseline and much better than nothing.

They are also all or nothing. You cannot exclude a service account, require more of administrators than of everyone else, trust the office network, or insist on a managed device. And because security defaults and conditional access cannot run at the same time, having them switched on means you have no conditional access policies at all. For a small business that is a sensible place to start. For a larger one it is usually the thing to grow out of.

Common questions

Is this safe to run?

It is read only. It uses Microsoft’s own Graph PowerShell module, asks for permissions that can only read, and changes nothing. It is printed in full on this page so you can check that for yourself before running it, which is what we would want you to do with any script.

Do you see my results?

No. The script prints to your screen. The analysis on this page runs in your browser. Nothing is uploaded, and there is no account or database behind any of it. If you want us to look, you have to send it to us deliberately.

Do I need to be a Global Administrator?

For the first run, yes. Microsoft requires an administrator to approve the read only permissions once before anybody in your tenant can use them, and no ordinary account can grant that approval. Once it has been given, Global Reader is sufficient, and is the better choice because it cannot change anything.

Why does it ask me to approve permissions?

The first time anyone in your tenant uses the Graph PowerShell module, Microsoft asks for its own Command Line Tools application to be approved. That approval is between you and Microsoft. We are not party to it, we gain nothing from it, and it grants us nothing. If the screen says Need admin approval, it means your account is not able to give it and an administrator has to.

Some checks came back as not available. Why?

A few of them depend on Entra ID P1 or on reports your licence may not include, the ninety day sign in check in particular. The script carries on and marks those as unavailable rather than failing.

What should I do with the result?

Fix multifactor coverage first, then legacy authentication, then trim the number of global administrators. Those three account for most of the real world account compromises we are called to.

Worth a look next: the email security checker looks at whether anyone can send email pretending to be your business, and takes about ten seconds.