Public/Get-EveryoneAccess.ps1
|
function Get-EveryoneAccess { <# .SYNOPSIS Finds every place the 'Everyone' or 'Everyone Except External Users' (EEEU) claim grants access - the #1 oversharing culprit that M365 Copilot amplifies. .DESCRIPTION Copilot can surface anything a user can already reach. The broad pseudo-groups 'Everyone' and 'Everyone except external users' are the most common way content ends up reachable by the whole company without anyone intending it. This cmdlet walks a site (or the whole tenant) and reports each web- and, optionally, list-level permission granted to those claims, along with the permission level and a direct URL. Read-only. Prereqs: PnP.PowerShell, and a connection (Connect-CopilotGovernance). For -AllSites you must be connected to the tenant-admin URL and pass -ClientId so each site can be reconnected using your cached token. .PARAMETER Site A single site collection URL to scan. .PARAMETER AllSites Scan every site in the tenant (requires an admin connection + -ClientId). .PARAMETER ClaimType Which claim(s) to hunt for: Everyone, EveryoneExceptExternal, or Both (default). .PARAMETER IncludeLists Also inspect lists/libraries that have unique (broken-inheritance) permissions. Slower, but catches oversharing below the site level. .PARAMETER ClientId Your PnP Entra app client id (needed to reconnect per-site during -AllSites). .EXAMPLE Connect-CopilotGovernance -Url "https://contoso.sharepoint.com/sites/hr" -ClientId $cid Get-EveryoneAccess -Site "https://contoso.sharepoint.com/sites/hr" -IncludeLists .EXAMPLE Connect-CopilotGovernance -Url "https://contoso-admin.sharepoint.com" -ClientId $cid Get-EveryoneAccess -AllSites -ClientId $cid | Export-Csv everyone-access.csv -NoTypeInformation #> [CmdletBinding(DefaultParameterSetName = 'Site')] [OutputType([pscustomobject])] param( [Parameter(Mandatory, ParameterSetName = 'Site', Position = 0)] [string]$Site, [Parameter(Mandatory, ParameterSetName = 'AllSites')] [switch]$AllSites, [ValidateSet('Everyone', 'EveryoneExceptExternal', 'Both')] [string]$ClaimType = 'Both', [switch]$IncludeLists, [switch]$IncludeRootSite, [string]$ClientId ) $claims = Get-CGClaim -ClaimType $ClaimType $results = [System.Collections.Generic.List[object]]::new() $skippedSites = [System.Collections.Generic.List[string]]::new() # coverage: sites we couldn't read # Work out which sites to scan if ($AllSites) { if (-not $ClientId) { throw "-AllSites requires -ClientId to reconnect to each site." } Write-Verbose "Enumerating tenant sites (admin connection required)..." $siteUrls = Get-PnPTenantSite -ErrorAction Stop -Verbose:$false | Where-Object { $_.Template -notmatch 'Redirect' -and # redirect (renamed/archived) sites $_.Template -notmatch 'SPSMSITEHOST' -and # the My Site host ($IncludeRootSite -or ([System.Uri]$_.Url).AbsolutePath -ne '/') # tenant root skipped by default (large + rarely the target) } | Select-Object -ExpandProperty Url Write-Verbose ("Found {0} site(s) to scan (root site {1})." -f @($siteUrls).Count, $(if ($IncludeRootSite) { 'included' } else { 'skipped - use -IncludeRootSite' })) } else { $siteUrls = @($Site) } $total = @($siteUrls).Count $i = 0 foreach ($siteUrl in $siteUrls) { $i++ if ($AllSites) { Write-Progress -Activity "Scanning for Everyone/EEEU access" -Status ("{0}/{1} {2}" -f $i, $total, $siteUrl) -PercentComplete (($i / [Math]::Max($total, 1)) * 100) Write-Verbose ("[{0}/{1}] {2}" -f $i, $total, $siteUrl) } # Connect to the target site if a ClientId is given (so -Site "just works" # without a prior Connect); otherwise use the current ambient connection. if ($AllSites -or $ClientId) { try { Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $ClientId -ErrorAction Stop -Verbose:$false } catch { Write-Warning "Could not connect to $siteUrl : $($_.Exception.Message)"; $skippedSites.Add($siteUrl); continue } } # Scan with bounded retry on transient throttling (HTTP 429/503). Accumulate # into a per-site list and only merge on success, so a retry never double-counts. $attempt = 0 while ($true) { $attempt++ $siteFindings = [System.Collections.Generic.List[object]]::new() try { $ctx = Get-PnPContext -Verbose:$false # --- Web-level role assignments --- $web = Get-PnPWeb -Includes RoleAssignments, Title, Url -Verbose:$false $ctx.Load($web.RoleAssignments) $ctx.ExecuteQuery() foreach ($ra in $web.RoleAssignments) { $ctx.Load($ra.Member); $ctx.Load($ra.RoleDefinitionBindings) } $ctx.ExecuteQuery() foreach ($ra in $web.RoleAssignments) { $claimName = Test-CGClaimLogin -LoginName $ra.Member.LoginName -Claims $claims if (-not $claimName) { continue } $roles = ($ra.RoleDefinitionBindings | ForEach-Object { $_.Name } | Where-Object { $_ -ne 'Limited Access' }) -join ', ' if (-not $roles) { continue } $siteFindings.Add([pscustomobject]@{ Site = $siteUrl; Scope = 'Site'; Object = $web.Title; Claim = $claimName; Permission = $roles; Url = $web.Url }) } # --- List-level role assignments (optional) --- if ($IncludeLists) { $lists = Get-PnPList -Includes HasUniqueRoleAssignments, RoleAssignments, Title, Hidden, DefaultViewUrl -Verbose:$false | Where-Object { -not $_.Hidden -and $_.HasUniqueRoleAssignments } foreach ($list in $lists) { $ctx.Load($list.RoleAssignments) $ctx.ExecuteQuery() foreach ($ra in $list.RoleAssignments) { $ctx.Load($ra.Member); $ctx.Load($ra.RoleDefinitionBindings) } $ctx.ExecuteQuery() foreach ($ra in $list.RoleAssignments) { $claimName = Test-CGClaimLogin -LoginName $ra.Member.LoginName -Claims $claims if (-not $claimName) { continue } $roles = ($ra.RoleDefinitionBindings | ForEach-Object { $_.Name } | Where-Object { $_ -ne 'Limited Access' }) -join ', ' if (-not $roles) { continue } $siteFindings.Add([pscustomobject]@{ Site = $siteUrl; Scope = 'List'; Object = $list.Title; Claim = $claimName; Permission = $roles; Url = $list.DefaultViewUrl }) } } } foreach ($f in $siteFindings) { $results.Add($f) } break } catch { $msg = $_.Exception.Message if (($msg -match '429|throttl|Too Many Requests|503|Service Unavailable') -and $attempt -lt 4) { $delay = [int](3 * [Math]::Pow(2, $attempt - 1)) Write-Warning ("Throttled on {0} (attempt {1}/3); retrying in {2}s..." -f $siteUrl, $attempt, $delay) Start-Sleep -Seconds $delay continue } Write-Warning "Skipped $siteUrl : $msg" $skippedSites.Add($siteUrl) break } } } if ($AllSites) { Write-Progress -Activity "Scanning for Everyone/EEEU access" -Completed } # Expose scan coverage so the orchestrator (and curious callers) can report blind spots. $script:CGCoverage = [pscustomobject]@{ Total = $total; Skipped = @($skippedSites) } # value-moment feedback nudge (once) Write-Host "" $line = " {0} 'Everyone/EEEU' grant(s) found" -f $results.Count if ($total -gt 1) { $line += " across {0} site(s) scanned" -f ($total - $skippedSites.Count) } Write-Host ($line + ".") -ForegroundColor Yellow if ($skippedSites.Count -gt 0) { Write-Host (" Coverage: {0} of {1} site(s) could not be read (see warnings) - results are partial." -f $skippedSites.Count, $total) -ForegroundColor DarkYellow } Write-Host " Useful? Star: https://github.com/gvijaikumar9/CopilotGovernance | Feedback: Send-CopilotGovernanceFeedback" -ForegroundColor DarkCyan return $results } |