This article provides a production-ready PnP.PowerShell script to enumerate Azure AD security groups (including Microsoft 365 Groups) that have permissions assigned on SharePoint Online sites across a tenant. The script has clearer variable names, avoids embedding secrets in-line, and exports results to a CSV.
What this script does
Authenticates to SharePoint Online using certificate-based app registration (recommended for unattended automation).
Enumerates all non-OneDrive tenant sites.
Reads site role assignments and filters by
SecurityGroupprincipal type.Outputs a CSV with site URL, group title, login name, and permission level.
Key security considerations
Do not hard-code secrets (client secrets, certificate passwords) in scripts. Use Azure Key Vault, managed identities, or prompt for secure input at runtime. The sample below uses placeholders and a secure prompt for the certificate password.
PowerShell script (fill the placeholders)
# Requires: PnP.PowerShell module
# Install-Module -Name PnP.PowerShell -Scope CurrentUser
# ----------------------
# Configuration (update only these variables)
# ----------------------
$TenantAdminUrl = 'https://-admin.sharepoint.com' # e.g. https://contoso-admin.sharepoint.com
$ClientId = '' # App (client) ID from Azure AD App Registration
$TenantId = '' # Directory (tenant) ID
$CertificatePath = 'C:\Path\To\certificate.pfx' # Full path to the certificate file
# Prompt securely for cert password at runtime
$CertificatePassword = Read-Host -Prompt 'Enter certificate password for the PFX (leave empty if none)' -AsSecureString
# Output path
$OutputPath = 'C:\Temp\SPOSecurityGroups.csv'
# Optional: filter sites by URL pattern (leave empty to include all)
$ExcludeUrlPattern = '*-my.sharepoint.com*' # excludes OneDrive MySites by default
# ----------------------
# Connect to tenant admin
# ----------------------
Connect-PnPOnline -Url $TenantAdminUrl -ClientId $ClientId -Tenant $TenantId -CertificatePath $CertificatePath -CertificatePassword $CertificatePassword
# Get tenant sites (excluding OneDrive/MySites)
$allSites = Get-PnPTenantSite -IncludeOneDriveSites:$false | Where-Object { $_.Url -notlike $ExcludeUrlPattern }
# Results container
$PermissionResults = @()
foreach ($site in $allSites) {
Write-Host "Checking site: $($site.Url)" -ForegroundColor Cyan
try {
# Connect to the site with the same app registration
Connect-PnPOnline -Url $site.Url -ClientId $ClientId -Tenant $TenantId -CertificatePath $CertificatePath -CertificatePassword $CertificatePassword
# Get web with role assignments (avoid unnecessary features checks)
$web = Get-PnPWeb -Includes RoleAssignments
$ctx = Get-PnPContext
# Load members and role definitions for each role assignment
foreach ($ra in $web.RoleAssignments) {
$ctx.Load($ra.Member)
$ctx.Load($ra.RoleDefinitionBindings)
}
$ctx.ExecuteQuery()
foreach ($ra in $web.RoleAssignments) {
$member = $ra.Member
# PrincipalType values: User, SharePointGroup, SecurityGroup, DistributionList, etc.
if ($member.PrincipalType -eq 'SecurityGroup') {
foreach ($roleDef in $ra.RoleDefinitionBindings) {
$PermissionResults += [PSCustomObject]@{
SiteUrl = $site.Url
GroupName = $member.Title
LoginName = $member.LoginName
PermissionLevel = $roleDef.Name
}
}
}
}
}
catch {
Write-Warning "Error accessing $($site.Url): $_"
}
}
# Export results
if ($PermissionResults.Count -gt 0) {
$PermissionResults | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host "Export completed: $OutputPath" -ForegroundColor Green
}
else {
Write-Host 'No security groups found with direct permissions on sites.' -ForegroundColor Yellow
}

Join the conversation! Your thoughts help the community grow.