SharePoint  

Audit SharePoint Security Groups — Script & Guide

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 SecurityGroup principal 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
}
  

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

  • Use Azure Key Vault: Store certificate passwords or client secrets in Key Vault and retrieve them at runtime.

  • Parallelization: For very large tenants, consider processing sites in parallel with ForEach-Object -Parallel (PowerShell 7+), but be careful with throttling.

  • Include site collections + subwebs: The script checks the root web role assignments. If you need lists/libraries or subwebs, extend the traversal.

  • Audit inheritance: You can expand the script to check list- and item-level unique permissions as required.

Troubleshooting

  • If you get errors about features (e.g., Push Notifications) when calling Get-PnPProperty , avoid that call and prefer Get-PnPWeb -Includes RoleAssignments as shown.

  • If you receive authentication failures, validate that the Azure AD App Registration has the proper Sites.FullControl.All (or the minimum required) application permission and that you have granted admin consent.

  • For slow runs, add logging and resume points (write partial CSVs) to avoid losing progress on long enumerations.

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:

  • Adapt this script to also include Microsoft 365 Groups and Azure AD Security Groups separately.

  • Add role inheritance checks and list-level scanning.

  • Convert to run in parallel and add retry/bakeoff for throttling.