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

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
}
  

Variable renames & rationale

OldNewWhy
$sites$allSitesMore descriptive: makes clear this is the collection of discovered sites.
$Results$PermissionResultsIndicates the data holds permission-related results.
$web$webKept same — conventional for SharePoint web object.
Inline secretsTop-level config variables + secure promptPrevents accidental secret exposure in source control.

Notes & improvements you can make

Troubleshooting

Sample CSV columns

SiteUrlGroupNameLoginNamePermissionLevel
https://contoso.sharepoint.com/sites/FinanceFinance SGi:0#.f|membership|[email protected]Edit

Wrap-up

This script gives you a repeatable way to create a cross-tenant inventory of Azure AD security groups that have permissions on SharePoint Online sites. Update the configuration section, secure your secrets, and consider running this from an automation account or scheduled runner with proper credentials stored securely.

If you want, I can: