Back to script library
Entra / Microsoft 365 · Applications

Find unused service principals

Find service principals that have not signed in to Microsoft 365 in the last year and generate statistics about service principals in the tenant.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -Scopes AuditLog.Read.All, Application.Read.All

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[string] $TenantId = "",
[int] $LookbackDays = 7
)
Connect-MgGraph -Scopes AuditLog.Read.All, Application.Read.All
Write-Host "Finding service principals..."
[array]$ServicePrincipals = Get-MgServicePrincipal -All -PageSize 500 | Sort-Object AppId
$SPHash = @{}
ForEach ($SP in $ServicePrincipals) {
$SPHash.Add($SP.AppId, $SP.DisplayName)
}
$CheckDate = [datetime]::UtcNow.AddDays(-$LookbackDays).ToString("s") + "Z"
# Output report
Write-Host "Fetching service principal sign-in activity data..."
$Report = [System.Collections.Generic.List[Object]]::new()
[array]$SPSignInLogs = Get-MgBetaReportServicePrincipalSignInActivity -Filter "(lastSignInActivity/lastSignInDateTime lt $CheckDate)" -All -PageSize 500
If (!($SPSignInLogs)) {
Write-Host "No sign-ins found for service principals"
Break
} Else {
Write-Host ("Found {0} sign-ins for service principals" -f $SPSignInLogs.Count)
}
Write-Host "Analyzing data..."
ForEach ($SPSignIn in $SPSignInLogs) {
$SPName = $SPHash[$SPSignIn.appId]
$DaysSince = (New-TimeSpan $SPSignIn.lastSignInActivity.lastSignInDateTime).Days
$ReportLine = [PSCustomObject]@{
'Service Principal Name' = $SPName
AppId = $SPSignin.AppId
LastSignIn = Get-Date $SPSignIn.lastSignInActivity.lastSignInDateTime -format 'dd-MMM-yyyy HH:mm:ss'
'Days Since Last Sign-In' = $DaysSince
}
$Report.Add($ReportLine)
}
$TenantReport = [System.Collections.Generic.List[Object]]::new()
$HomeTenant = (Get-MgOrganization).DisplayName
[array]$TenantIds = $ServicePrincipals | Sort-Object AppOwnerOrganizationId -Unique | Select-Object -ExpandProperty AppOwnerOrganizationId
ForEach ($TenantId in $TenantIds) {
$NumberApps = ($ServicePrincipals | Where-Object {$_.AppOwnerOrganizationId -eq $TenantId}).Count
$Uri = ("https://graph.microsoft.com/V1.0/tenantRelationships/findTenantInformationByTenantId(tenantId='{0}')" -f $TenantId.ToString())
$TenantData = Invoke-MgGraphRequest -Uri $Uri -Method Get
$ReportLine = [PSCustomObject]@{
'Tenant Name' = $TenantData.DisplayName
'Tenant ID' = $TenantId
'Number of Apps'= $NumberApps
}
$TenantReport.Add($ReportLine)
}
$TenantReport = $TenantReport | Sort-Object 'Number of apps' -Descending
[array]$AppsNoName = $Report | Where-Object {$_.'Service Principal Name' -eq $null}
Write-Host ("Some notes about service principals for the {0} tenant" -f $HomeTenant)
Write-Host "------------------------------------------------------------------------"
Write-Host ""
Write-Host "Service Principals by owning tenant"
$TenantReport | Format-Table -AutoSize
Write-Host ""
Write-Host ("Total Service Principals {0}" -f $ServicePrincipals.Count)
Write-Host ("Service Principals with no sign-ins in the last year {0}" -f $Report.Count)
Write-Host ("Service Principals with sign-ins in the last year {0}" -f ($ServicePrincipals.Count - $Report.Count))
Write-Host ("Number of apps with no service principal {0}" -f $AppsNoName.Count)
Write-Host ""
# Generate some reports
$Report | Out-GridView -Title "Service Principals with no sign-ins in the last year"
$Report | Export-CSV -Path ServicePrincpalsNoSignIn.csv -NoTypeInformation -Encoding UTF8
Write-Host "Report detailing service principals with last sign-in longer than a year written to ServicePrincipalsNoSignIn.csv"

Parameters

ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.
Attribution