Emergency server help: get in touch

Entra ID Inactive Users Report: signInActivity PowerShell Script

Free PowerShell script that lists inactive Microsoft Entra ID users from signInActivity, with guests, licensed-only and never-signed-in filters and CSV output.

Version
1.0.0
Last updated
October 6, 2026
Language
PowerShell
Tested on
Syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25 on 6 Oct 2026; not yet run against a live tenant/server.
License
MIT
Pricing
Free

Short answer: run .\Get-EntraInactiveUsers.ps1 -Days 90 -CsvPath .\inactive.csv. It reads every user’s signInActivity from Microsoft Graph and lists accounts whose last successful sign-in (lastSuccessfulSignInDateTime) is older than -Days, plus accounts that never signed in. Add -LicensedOnly to find paid licences you can reclaim, or -UserType Guest for stale guests. signInActivity needs a Microsoft Entra ID P1 or P2 licence in the tenant and the AuditLog.Read.All permission.

Commands checked against the official documentation (linked below) on 6 October 2026; not yet run on our lab servers. The script was syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25 (no errors or warnings) but has not yet been run against a live Microsoft 365 tenant. Every cmdlet, endpoint, property and permission it uses was checked against Microsoft Learn. If something behaves differently for you, tell us and we will fix the script.

What it does

This is the cloud version of our inactive AD users guide. Entra ID does not have lastLogonTimestamp; it has the signInActivity property on each user, with three dates:

PropertyWhat Microsoft records
lastSignInDateTimeLast interactive sign-in attempt, successful or not
lastNonInteractiveSignInDateTimeLast non-interactive sign-in attempt (a client using a token on the user’s behalf), successful or not
lastSuccessfulSignInDateTimeLast successful interactive or non-interactive sign-in. Microsoft recommends it for inactive-user reports

Because the first two include failures, an account that is being password-sprayed looks “active” if you go by them. The script therefore uses lastSuccessfulSignInDateTime by default (-Basis Successful). Microsoft did not backfill that property when it was introduced, so if it is empty the script falls back to the newest of the other two dates and says so in the BasisUsed column. -Basis AnyAttempt uses the newest of all three instead.

  • Members and guests: -UserType Member, Guest or All (default). Guests also get their invitation state (externalUserState).
  • Licensed only: -LicensedOnly keeps users with at least one entry in assignedLicenses.
  • New accounts created inside the window are skipped unless you add -IncludeRecentlyCreated, so last week’s starters do not show up as “never signed in”.
  • Disabled accounts are skipped unless -IncludeDisabled.
  • Synced users are flagged (OnPremisesSynced) because they must be disabled in on-premises AD, not in Entra ID.
  • Read-only: the script only lists accounts. Disabling or deleting them is a separate, deliberate step.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7 with the Microsoft.Graph.Authentication module.
  • Licence: Microsoft Entra ID P1 or P2 in the tenant. Without it Graph refuses signInActivity and the script stops with an explanation.
  • Permissions: User.Read.All and AuditLog.Read.All (delegated or application).
  • Role: Microsoft names Reports Reader as the least privileged role for reading sign-in activity; Global Reader also works.

Download and first run

  1. Copy the script from the box on this page (or use the download button) and save it as C:\Scripts\Get-EntraInactiveUsers.ps1.
  2. If you downloaded the file, clear the “downloaded from the internet” mark so the execution policy lets it run: Unblock-File C:\Scripts\Get-EntraInactiveUsers.ps1.
  3. Install the module once, for your user: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
  4. Read the built-in help, then run it once interactively and look at the result on screen before you export or schedule anything.
cd C:\Scripts
Get-Help .\Get-EntraInactiveUsers.ps1 -Full
.\Get-EntraInactiveUsers.ps1 -Days 90
.\Get-EntraInactiveUsers.ps1 -Days 90 -LicensedOnly -UserType Member

Options

ParameterWhat it doesDefault
-DaysInactive for at least this many days (1 to 3650)90
-UserTypeAll, Member or GuestAll
-LicensedOnlyOnly users with an assigned licenceOff
-IncludeDisabledAlso report blocked accountsOff
-IncludeRecentlyCreatedAlso report accounts created less than -Days agoOff
-BasisSuccessful (last successful sign-in) or AnyAttempt (newest of all three dates)Successful
-CsvPath / -HtmlPathWrite CSV and/or HTMLScreen only
-PassThruSend objects down the pipelineOff
-TenantId, -ClientId, -CertificateThumbprintApp-only sign-in for scheduled runsInteractive

Usage examples

# Everyone inactive for 90 days, to CSV
.\Get-EntraInactiveUsers.ps1 -Days 90 -CsvPath C:\Reports\inactive-90.csv

# Licences to reclaim: licensed members idle for 60 days
.\Get-EntraInactiveUsers.ps1 -Days 60 -LicensedOnly -UserType Member -HtmlPath C:\Reports\licensed-idle.html

# Stale guests, including ones already blocked
.\Get-EntraInactiveUsers.ps1 -Days 180 -UserType Guest -IncludeDisabled -CsvPath .\stale-guests.csv

# Accounts that never signed in at all
.\Get-EntraInactiveUsers.ps1 -Days 30 -PassThru | Where-Object { -not $_.LastActivity }

CSV columns

No sample output is shown because the script has not yet run against a live tenant. Columns:

ColumnMeaning
DisplayName, UserPrincipalName, UserType, AccountEnabledFrom GET /users
IsLicensed, LicenseCountFrom assignedLicenses
CreatedDateTimeWhen the account was created (UTC)
LastSuccessfulSignIn, LastInteractiveSignIn, LastNonInteractiveSignInThe three signInActivity dates (UTC)
LastActivity, DaysInactiveThe date the script judged by, and whole days since then
Status“Inactive N days” or “NeverSignedIn (or last sign-in before April 2020)”
BasisUsedLastSuccessful, AnyAttempt, or “Fallback: last attempt (no successful sign-in recorded)”
OnPremisesSyncedTrue for users synced from AD (disable them there)
ExternalUserStateGuest invitation state (for example PendingAcceptance)
IdObject ID, handy for follow-up scripts

Schedule it

Scheduled runs cannot answer a sign-in prompt, so use app-only sign-in with a certificate. Microsoft’s steps are in Use app-only authentication with the Microsoft Graph PowerShell SDK; in short:

  1. Create an app registration in the Microsoft Entra admin center (single tenant).
  2. Create a certificate on the machine that will run the task, install it in the certificate store of the account that runs the task, and upload the public key (.cer) to the app registration.
  3. Under API permissions add these Application permissions for Microsoft Graph and grant admin consent: User.Read.All and AuditLog.Read.All.
  4. Note the application (client) ID, the tenant ID and the certificate thumbprint.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Get-EntraInactiveUsers.ps1 -Days 90 -LicensedOnly -CsvPath C:\Reports\inactive.csv -TenantId contoso.onmicrosoft.com -ClientId <app-id> -CertificateThumbprint <thumbprint>'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 7am
Register-ScheduledTask -TaskName 'Entra inactive users' -Action $action -Trigger $trigger -User 'CONTOSO\svc-reports' -Password '<password>'

Run the task as the account whose certificate store holds the certificate (a dedicated service account works well), and keep the private key on that machine only. The app has tenant-wide read access, so treat the certificate like an admin password and set an expiry date you will notice. The CSV is overwritten on every run; add the date to the file name in a small wrapper if you want history.

How it works

  1. Connect-MgGraph with User.Read.All and AuditLog.Read.All (or app-only with the certificate).
  2. One paged query: GET /users?$select=id,displayName,userPrincipalName,userType,accountEnabled,createdDateTime,assignedLicenses,onPremisesSyncEnabled,externalUserState,signInActivity&$top=500. Microsoft limits pages to 500 users when signInActivity is selected; the script follows @odata.nextLink.
  3. Filtering is done locally. Microsoft does not allow signInActivity to be filtered together with other properties in the same query, so reading everything once and filtering in PowerShell is simpler and avoids that restriction.
  4. For each user the script picks the date according to -Basis, compares it with the cut-off (now minus -Days, in UTC) and keeps the user if it is older or missing.
  5. Results are sorted with never-signed-in accounts first, then by days inactive.

Limitations

  • Up to 24 hours behind. Microsoft notes the last sign-in values can take up to a day to update. Do not use a 1-day window.
  • History limits. Microsoft keeps interactive sign-ins back to April 2020 and non-interactive back to May 2020, so an empty value can also mean “not since then”.
  • Guests sign in through their home tenant; for guest clean-up also look at Entra ID access reviews.
  • Service and shared-mailbox accounts often never sign in on purpose. Check before you act on the NeverSignedIn rows.
  • Before disabling anyone, export the CSV, confirm with the account owners, and disable first rather than delete; deleted users can be restored for 30 days.
  • Not yet run against a live tenant (see the note at the top).

Official documentation: signInActivity resource type · How to manage inactive user accounts · List users (page size with signInActivity)

Related: Find Inactive AD Users and Computers: PowerShell Cleanup in 5 Steps · Employee Offboarding Checklist: Secure AD, M365 and Workspace Steps · Get-ADUser PowerShell Examples: 25 Queries for Active Directory · Microsoft 365 shared mailbox vs distribution group vs Microsoft 365 Group

See also: Microsoft 365 MFA Status Report: PowerShell Script for Graph · Windows LAPS with Intune and Entra ID: Setup and Retrieval · Intune Device Compliance Report: PowerShell Script via Graph · Microsoft 365 User Offboarding PowerShell Script (with -WhatIf)

The script

Get-EntraInactiveUsers.ps1Download
# Entra ID Inactive Users Report: signInActivity PowerShell Script (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/entra-inactive-users-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
    Find inactive Microsoft Entra ID (Microsoft 365) users from signInActivity: members and guests,
    licensed or not, with last successful, interactive and non-interactive sign-in dates. CSV/HTML output.

.DESCRIPTION
    Read-only. Lists users through Microsoft Graph (GET /users with $select=signInActivity) using the
    Microsoft.Graph.Authentication module, then works out how many days each account has been inactive.

    Which date counts as "last activity" (-Basis):
      Successful (default)  lastSuccessfulSignInDateTime (interactive or non-interactive, successful only).
                            Microsoft started filling this property on 1 December 2023 and did not backfill it,
                            so when it is empty the script falls back to the newest of lastSignInDateTime and
                            lastNonInteractiveSignInDateTime and says so in the BasisUsed column.
      AnyAttempt            Newest of all three dates. lastSignInDateTime and lastNonInteractiveSignInDateTime
                            also record FAILED attempts, so a password-spray target can look "active".

    Accounts with no sign-in data at all are reported as NeverSignedIn (or "last sign-in before April 2020").
    Accounts created inside the -Days window are skipped unless -IncludeRecentlyCreated is used.

    Requirements (Microsoft Learn): signInActivity needs a Microsoft Entra ID P1 or P2 licence in the tenant
    and the AuditLog.Read.All permission (plus User.Read.All). Delegated: the signed-in admin needs at least
    the Reports Reader role (Global Reader also works). App-only: User.Read.All and AuditLog.Read.All as
    Application permissions with admin consent. The value can lag real activity by up to 24 hours.

.PARAMETER Days                    Inactive for at least this many days (default 90).
.PARAMETER UserType                All (default), Member or Guest.
.PARAMETER LicensedOnly            Only users with at least one assigned licence.
.PARAMETER IncludeDisabled         Also report disabled accounts (default: enabled accounts only).
.PARAMETER IncludeRecentlyCreated  Also report accounts created less than -Days ago.
.PARAMETER Basis                   Successful (default) or AnyAttempt. See DESCRIPTION.
.PARAMETER CsvPath                 Write the result to this CSV file.
.PARAMETER HtmlPath                Write the result to this HTML file.
.PARAMETER PassThru                Output the objects to the pipeline.
.PARAMETER TenantId                Tenant ID or domain (required for app-only sign-in).
.PARAMETER ClientId                App registration (client) ID for app-only sign-in.
.PARAMETER CertificateThumbprint   Certificate thumbprint for app-only sign-in.

.EXAMPLE  .\Get-EntraInactiveUsers.ps1 -Days 90 -CsvPath .\inactive-90.csv
.EXAMPLE  .\Get-EntraInactiveUsers.ps1 -Days 60 -LicensedOnly -UserType Member -HtmlPath .\licensed-inactive.html
.EXAMPLE  .\Get-EntraInactiveUsers.ps1 -Days 180 -UserType Guest -IncludeDisabled -CsvPath .\stale-guests.csv
.EXAMPLE  .\Get-EntraInactiveUsers.ps1 -TenantId contoso.onmicrosoft.com -ClientId <app-id> -CertificateThumbprint <thumbprint> -CsvPath C:\Reports\inactive.csv

.NOTES
    Name:     Get-EntraInactiveUsers.ps1
    Purpose:  Inactive user report for Microsoft Entra ID based on signInActivity
    Source:   https://srvscripts.com/scripts/entra-inactive-users-report/
    License:  MIT
    Version:  1.0.0
    Requires: Windows PowerShell 5.1 or PowerShell 7, module Microsoft.Graph.Authentication.
#>
[CmdletBinding(DefaultParameterSetName = 'Interactive')]
param(
    [ValidateRange(1, 3650)]
    [int]$Days = 90,
    [ValidateSet('All', 'Member', 'Guest')]
    [string]$UserType = 'All',
    [switch]$LicensedOnly,
    [switch]$IncludeDisabled,
    [switch]$IncludeRecentlyCreated,
    [ValidateSet('Successful', 'AnyAttempt')]
    [string]$Basis = 'Successful',
    [string]$CsvPath,
    [string]$HtmlPath,
    [switch]$PassThru,
    [Parameter(ParameterSetName = 'Interactive')]
    [Parameter(ParameterSetName = 'AppOnly', Mandatory = $true)]
    [string]$TenantId,
    [Parameter(ParameterSetName = 'AppOnly', Mandatory = $true)]
    [ValidatePattern('^[0-9a-fA-F-]{36}$')]
    [string]$ClientId,
    [Parameter(ParameterSetName = 'AppOnly', Mandatory = $true)]
    [ValidatePattern('^[0-9a-fA-F]{40}$')]
    [string]$CertificateThumbprint
)

Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'
$Graph = 'https://graph.microsoft.com/v1.0'

function Write-Status([string]$Message) { Write-Information $Message -InformationAction Continue }

function Invoke-GraphWithRetry {
    param([string]$Uri)
    $attempt = 0
    while ($true) {
        $attempt++
        try {
            return Invoke-MgGraphRequest -Method GET -Uri $Uri -OutputType HashTable -ErrorAction Stop
        } catch {
            if ($attempt -lt 5 -and $_.Exception.Message -match '429|TooManyRequests|503|ServiceUnavailable|504|GatewayTimeout') {
                Start-Sleep -Seconds ([math]::Pow(2, $attempt) * 5)
                continue
            }
            throw
        }
    }
}

function Get-GraphCollection([string]$Uri) {
    $next = $Uri
    while ($next) {
        $r = Invoke-GraphWithRetry -Uri $next
        if ($r['value']) { foreach ($i in $r['value']) { $i } }
        $next = $r['@odata.nextLink']
    }
}

function Get-Value($Hash, [string]$Key) {
    if ($null -ne $Hash -and $Hash.ContainsKey($Key)) { return $Hash[$Key] }
    return $null
}

function ConvertTo-UtcDate($Value) {
    if ($null -eq $Value -or "$Value" -eq '') { return $null }
    if ($Value -is [datetime]) { return $Value.ToUniversalTime() }
    if ($Value -is [datetimeoffset]) { return $Value.UtcDateTime }
    return ([datetimeoffset]::Parse([string]$Value, [Globalization.CultureInfo]::InvariantCulture)).UtcDateTime
}

function Get-Newest([object[]]$Dates) {
    $d = @($Dates | Where-Object { $null -ne $_ } | Sort-Object -Descending)
    if ($d.Count) { return $d[0] }
    return $null
}

# ---- Connect --------------------------------------------------------------------------------------------
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
    throw 'Module Microsoft.Graph.Authentication is not installed. Run: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}
Import-Module Microsoft.Graph.Authentication
if ($PSCmdlet.ParameterSetName -eq 'AppOnly') {
    Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome
} else {
    $cp = @{ Scopes = @('User.Read.All', 'AuditLog.Read.All'); NoWelcome = $true }
    if ($TenantId) { $cp.TenantId = $TenantId }
    Connect-MgGraph @cp
}
$ctx = Get-MgContext
if (-not $ctx) { throw 'Not connected to Microsoft Graph.' }

# ---- Read users -----------------------------------------------------------------------------------------
$select = 'id,displayName,userPrincipalName,userType,accountEnabled,createdDateTime,assignedLicenses,onPremisesSyncEnabled,externalUserState,signInActivity'
Write-Status 'Reading users and signInActivity (500 per page)...'
try {
    $users = @(Get-GraphCollection -Uri "$Graph/users?`$select=$select&`$top=500")
} catch {
    if ($_.Exception.Message -match 'Forbidden|403|premium|license|Authorization_RequestDenied') {
        throw "Graph refused signInActivity: $($_.Exception.Message). It needs a Microsoft Entra ID P1/P2 licence in the tenant, the AuditLog.Read.All permission and (delegated) at least the Reports Reader role."
    }
    throw
}
Write-Status ("{0} user objects read." -f $users.Count)

$now = (Get-Date).ToUniversalTime()
$cutoff = $now.AddDays(-$Days)
$rows = [System.Collections.Generic.List[object]]::new()

foreach ($u in $users) {
    $type = [string](Get-Value $u 'userType')
    if ($UserType -ne 'All' -and $type -ne $UserType) { continue }
    $enabled = Get-Value $u 'accountEnabled'
    if (-not $IncludeDisabled -and $enabled -eq $false) { continue }
    $licCount = @(@(Get-Value $u 'assignedLicenses') | Where-Object { $_ }).Count
    if ($LicensedOnly -and $licCount -eq 0) { continue }
    $created = ConvertTo-UtcDate (Get-Value $u 'createdDateTime')
    if (-not $IncludeRecentlyCreated -and $created -and $created -gt $cutoff) { continue }

    $sia = Get-Value $u 'signInActivity'
    $succ = ConvertTo-UtcDate (Get-Value $sia 'lastSuccessfulSignInDateTime')
    $inter = ConvertTo-UtcDate (Get-Value $sia 'lastSignInDateTime')
    $nonInter = ConvertTo-UtcDate (Get-Value $sia 'lastNonInteractiveSignInDateTime')

    if ($Basis -eq 'AnyAttempt') {
        $last = Get-Newest @($succ, $inter, $nonInter); $used = 'AnyAttempt'
    } elseif ($succ) {
        $last = $succ; $used = 'LastSuccessful'
    } else {
        $last = Get-Newest @($inter, $nonInter); $used = if ($last) { 'Fallback: last attempt (no successful sign-in recorded)' } else { '' }
    }
    if ($last -and $last -gt $cutoff) { continue }

    $daysInactive = if ($last) { [int][math]::Floor(($now - $last).TotalDays) } else { $null }
    $rows.Add([pscustomobject][ordered]@{
        DisplayName              = Get-Value $u 'displayName'
        UserPrincipalName        = Get-Value $u 'userPrincipalName'
        UserType                 = $type
        AccountEnabled           = $enabled
        IsLicensed               = ($licCount -gt 0)
        LicenseCount             = $licCount
        CreatedDateTime          = $created
        LastSuccessfulSignIn     = $succ
        LastInteractiveSignIn    = $inter
        LastNonInteractiveSignIn = $nonInter
        LastActivity             = $last
        DaysInactive             = $daysInactive
        Status                   = if ($last) { "Inactive $daysInactive days" } else { 'NeverSignedIn (or last sign-in before April 2020)' }
        BasisUsed                = $used
        OnPremisesSynced         = [bool](Get-Value $u 'onPremisesSyncEnabled')
        ExternalUserState        = Get-Value $u 'externalUserState'
        Id                       = Get-Value $u 'id'
    })
}

$out = @($rows | Sort-Object @{ e = { if ($null -eq $_.DaysInactive) { [int]::MaxValue } else { $_.DaysInactive } }; Descending = $true }, UserPrincipalName)

if ($CsvPath) {
    $out | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
    Write-Status ("CSV written: {0}" -f $CsvPath)
}
if ($HtmlPath) {
    $css = '<style>body{font-family:Segoe UI,Arial,sans-serif;font-size:13px}table{border-collapse:collapse}th,td{border:1px solid #ccc;padding:4px 6px;text-align:left}th{background:#eee}</style>'
    $pre = "<h2>Inactive Entra ID users ($Days+ days)</h2><p>Tenant $($ctx.TenantId). Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Basis: $Basis. Users: $($out.Count).</p>"
    $out | Select-Object -Property * -ExcludeProperty Id | ConvertTo-Html -Head $css -PreContent $pre | Out-File -FilePath $HtmlPath -Encoding UTF8
    Write-Status "HTML written: $HtmlPath"
}

$never = @($out | Where-Object { $null -eq $_.LastActivity }).Count
$guests = @($out | Where-Object { $_.UserType -eq 'Guest' }).Count
$lic = @($out | Where-Object { $_.IsLicensed }).Count
Write-Status ("Inactive {0}+ days: {1} (guests {2}, licensed {3}, never signed in {4})." -f $Days, $out.Count, $guests, $lic, $never)

if ($PassThru) { return $out }
if (-not $CsvPath -and -not $HtmlPath) {
    $out | Select-Object DisplayName, UserPrincipalName, UserType, IsLicensed, AccountEnabled, LastActivity, DaysInactive | Format-Table -AutoSize
}
Version 1.0.0 · SHA-256 94faf62699f2536e57e507f5ebcf124602155c47e4b956407223d6f5c052eb97
Download and verify on Linux or macOS
curl -fsSL -o Get-EntraInactiveUsers.ps1 https://scr.srvscripts.com/entra-inactive-users-report/Get-EntraInactiveUsers.ps1 && curl -fsSL https://scr.srvscripts.com/entra-inactive-users-report/Get-EntraInactiveUsers.ps1.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/entra-inactive-users-report/Get-EntraInactiveUsers.ps1' -OutFile 'Get-EntraInactiveUsers.ps1'; if ((Get-FileHash 'Get-EntraInactiveUsers.ps1' -Algorithm SHA256).Hash -eq '94FAF62699F2536E57E507F5EBCF124602155C47E4B956407223D6F5C052EB97') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

How do I find inactive users in Microsoft 365?

Read signInActivity for each user through Microsoft Graph and compare lastSuccessfulSignInDateTime with your cut-off date. This script does that for the whole tenant and exports a CSV.

Why is signInActivity empty or refused?

It needs a Microsoft Entra ID P1 or P2 licence in the tenant and the AuditLog.Read.All permission. An empty value on a single user means no recorded sign-in.

Which date should I use for inactive users?

lastSuccessfulSignInDateTime. The other two dates also record failed attempts, so an attacked account can look active.

Which admin role is needed?

Microsoft lists Reports Reader as the least privileged role that can read sign-in activity. Global Reader also works.

Does the script disable inactive users?

No. It only reports. Review the list, then disable accounts in Entra ID, or in on-premises AD for synced users.

Changelog

  • 1.0.0 — First release.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.