scripts/local/sharepoint-admin.ps1
|
<# Raidiness — SharePoint Online (local, read-only) Why local: the SharePoint Online Management Shell app-only requires Sites.FullControl.All. That is write access to every site in the tenant, and Raidiness never asks for it (CLAUDE.md #1, docs/10-readonly-policy.md §3). A SharePoint Administrator runs this script interactively; it uses the same rights the administrator already has and changes nothing. Role: SharePoint Administrator. Writes: nothing. Only Get-* cmdlets and a JSON file on your own disk. Output: JSON you upload in Raidiness under the "SharePoint Online" module. Usage: .\sharepoint-admin.ps1 -TenantName contoso -RunId <run> -OutputPath .\spo.json .\sharepoint-admin.ps1 -TenantName contoso -RunId <run> -SampleSize 25 #> [CmdletBinding()] param( # The name before -admin.sharepoint.com, so "contoso" for contoso-admin.sharepoint.com. [Parameter(Mandatory = $true)][string] $TenantName, [Parameter(Mandatory = $true)][string] $RunId, [string] $OutputPath = ".\raidiness-sharepoint.json", # Reading unique permissions takes time per site; a sample by default. [int] $SampleSize = 15, [switch] $HashUpns ) $ErrorActionPreference = "Stop" function Protect-Upn { param([string] $Value) if (-not $HashUpns -or [string]::IsNullOrWhiteSpace($Value)) { return $Value } $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value.ToLowerInvariant()) $sha = [System.Security.Cryptography.SHA256]::Create() return (($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) -join "").Substring(0, 16) } Write-Host "Raidiness — SharePoint Online, read-only. Role: SharePoint Administrator." -ForegroundColor Cyan Write-Host "Nothing will be changed in the tenant." -ForegroundColor Cyan Connect-SPOService -Url "https://$TenantName-admin.sharepoint.com" $tenant = Get-SPOTenant | Select-Object ` @{ Name = 'SharingCapability'; Expression = { [string]$_.SharingCapability } }, @{ Name = 'DefaultSharingLinkType'; Expression = { [string]$_.DefaultSharingLinkType } }, @{ Name = 'DefaultLinkPermission'; Expression = { [string]$_.DefaultLinkPermission } }, @{ Name = 'ConditionalAccessPolicy'; Expression = { [string]$_.ConditionalAccessPolicy } }, @{ Name = 'LimitedAccessFileType'; Expression = { [string]$_.LimitedAccessFileType } }, RequireAnonymousLinksExpireInDays, ExternalUserExpirationRequired, IsCollabMeetingNotesFluidEnabled $searchMode = $null try { $searchMode = Get-SPOTenantRestrictedSearchMode | Select-Object RestrictedSearchMode } catch { Write-Host "Restricted SharePoint Search is not available in this tenant." -ForegroundColor Yellow } $sites = Get-SPOSite -Limit All | Select-Object ` Url, Title, Owner, StorageUsageCurrent, LastContentModifiedDate, SharingCapability, RestrictedContentDiscovery, SensitivityLabel $restricted = $sites | Where-Object { $_.RestrictedContentDiscovery -eq $true } foreach ($site in $sites) { $site.Owner = Protect-Upn $site.Owner } # Unique permissions are expensive to fetch per site; a sample of the largest # sites is enough to show whether the pattern occurs. $uniquePermissions = @() if (Get-Module -ListAvailable -Name PnP.PowerShell) { Import-Module PnP.PowerShell $sample = $sites | Sort-Object StorageUsageCurrent -Descending | Select-Object -First $SampleSize foreach ($site in $sample) { try { Connect-PnPOnline -Url $site.Url -Interactive $lists = Get-PnPList -Includes HasUniqueRoleAssignments | Where-Object { $_.HasUniqueRoleAssignments -eq $true } $uniquePermissions += [ordered]@{ url = $site.Url listsWithUniquePermissions = @($lists).Count lists = @($lists | Select-Object -First 20 | ForEach-Object { $_.Title }) } } catch { Write-Host "Skipped (no access): $($site.Url)" -ForegroundColor Yellow } } } else { Write-Host "PnP.PowerShell is missing; unique permissions will be skipped." -ForegroundColor Yellow } $payload = [ordered]@{ raidiness = [ordered]@{ version = "1.0" module = "sharepoint-admin" tenantId = $TenantName runId = $RunId generatedAt = (Get-Date).ToUniversalTime().ToString("o") upnsHashed = [bool] $HashUpns } results = [ordered]@{ "spo.tenant" = $tenant "spo.tenantRestrictedSearchMode" = $searchMode "spo.sites" = $sites "spo.restrictedContentDiscovery" = @($restricted | Select-Object Url, Title) "pnp.uniquePermissionsSample" = $uniquePermissions } } $payload | ConvertTo-Json -Depth 8 | Out-File -FilePath $OutputPath -Encoding utf8 Write-Host "Done. Upload $OutputPath in Raidiness under the SharePoint Online module." -ForegroundColor Green |