<# .SYNOPSIS [Parameter(Mandatory = $true)].SYNOPSIS [string]$OutputCsv, [string]$SiteCollectionUrl, [switch]$IncludeItemLevel, [switch]$NoPrompt, [switch]$Remediate ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Write-Host "" Write-Host "SCRIPT 06 - PERMISSIONS AUDIT REPORT" -ForegroundColor Cyan Write-Host "READ-ONLY. NO CHANGES ARE MADE." -ForegroundColor Green Write-Host "" # ------------------------------------------------------------ # Load SharePoint PowerShell snap-in # ------------------------------------------------------------ try { if (-not (Get-PSSnapin -Name "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue)) { Add-PSSnapin "Microsoft.SharePoint.PowerShell" } } catch { throw "Unable to load SharePoint snap-in. Run in SharePoint Management Shell. Error: $($_.Exception.Message)" } # ------------------------------------------------------------ # Output setup # ------------------------------------------------------------ $outDir = Split-Path -Path $OutputCsv -Parent if ([string]::IsNullOrWhiteSpace($outDir)) { throw "OutputCsv must be a full path. Example: C:\Temp\Permissions_Audit.csv" } if (-not (Test-Path -Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null } $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss") $baseName = [System.IO.Path]::GetFileNameWithoutExtension($OutputCsv) $summaryPath = Join-Path $outDir ("{0}_{1}_Summary_ByRiskLevel.csv" -f $baseName, $timestamp) $logPath = Join-Path $outDir ("{0}_{1}_RunLog.txt" -f $baseName, $timestamp) $errorPath = Join-Path $outDir ("{0}_{1}_Errors.csv" -f $baseName, $timestamp) # ------------------------------------------------------------ # Error / log helpers # ------------------------------------------------------------ $errors = New-Object System.Collections.Generic.List[object] function Add-ErrorRecord { param( [string]$Stage, [string]$Scope, [string]$ObjectTitle, [string]$Message ) $errors.Add([pscustomobject]@{ Timestamp = Get-Date Stage = $Stage Scope = $Scope ObjectTitle = $ObjectTitle Message = $Message }) | Out-Null } $runMessages = New-Object System.Collections.Generic.List[string] function Log-Message { param([string]$Message) $stamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") $line = "[{0}] {1}" -f $stamp, $Message $runMessages.Add($line) | Out-Null Write-Host $line } if ($Remediate) { Write-Host "Note: -Remediate is included for toolkit consistency only. This script remains READ-ONLY." -ForegroundColor Yellow } if (-not $NoPrompt) { Write-Host "This script scans SharePoint permissions and writes reports only." -ForegroundColor Yellow Write-Host "It does not modify permissions, content, or configuration." -ForegroundColor Yellow if ($IncludeItemLevel) { Write-Host "Item-level scanning is enabled and may significantly increase runtime." -ForegroundColor Yellow } $resp = Read-Host "Type YES to continue" if ($resp -ne "YES") { Write-Host "Cancelled by user." -ForegroundColor Yellow return } } # ------------------------------------------------------------ # Resolve target sites # ------------------------------------------------------------ function Resolve-SitesToScan { param( [string]$WebAppUrl, [string]$SiteCollectionUrl ) if (-not [string]::IsNullOrWhiteSpace($SiteCollectionUrl)) { return @(Get-SPSite -Identity $SiteCollectionUrl) } return @(Get-SPSite -WebApplication $WebAppUrl -Limit All) } # ------------------------------------------------------------ # Helper functions # ------------------------------------------------------------ function Get-PermissionNames { param($RoleAssignment) try { return (@($RoleAssignment.RoleDefinitionBindings | ForEach-Object { $_.Name }) -join "; ") } catch { return "" } } function Get-MemberCountEstimate { param($Member) try { if ($Member -is [Microsoft.SharePoint.SPGroup]) { return @($Member.Users).Count } } catch { } return 1 } function Get-ExposureType { param( [string]$PrincipalType, [string]$PermissionLevels ) if ($PrincipalType -match "SharePointGroup|SecurityGroup") { return "GroupAccess" } if ($PermissionLevels -match "Full Control|Design") { return "ElevatedDirectAccess" } return "DirectAccess" } function Get-RiskLevel { param( [bool]$HasUniquePermissions, [string]$PermissionLevels, [int]$MemberCountEstimate, [string]$ScopeType ) $score = 0 if ($HasUniquePermissions) { $score += 25 } if ($PermissionLevels -match "Full Control") { $score += 30 } elseif ($PermissionLevels -match "Design") { $score += 20 } if ($MemberCountEstimate -ge 25) { $score += 20 } elseif ($MemberCountEstimate -ge 10) { $score += 10 } if ($ScopeType -eq "Item") { $score += 10 } if ($score -ge 50) { return "High" } if ($score -ge 20) { return "Medium" } return "Low" } function Get-Score { param([string]$RiskLevel) switch ($RiskLevel) { "High" { return 30 } "Medium" { return 60 } "Low" { return 90 } default { return 50 } } } function Get-Recommendation { param( [string]$ScopeType, [string]$RiskLevel, [bool]$HasUniquePermissions ) if ($ScopeType -eq "Item") { return "Review item-level unique permissions. Consider simplifying before migration if no longer needed." } if ($HasUniquePermissions -and $RiskLevel -eq "High") { return "Validate broken inheritance, confirm least privilege, and document business justification before migration." } if ($HasUniquePermissions) { return "Review whether unique permissions are still required." } return "Keep in baseline permissions inventory and validate with owners during migration planning." } # ------------------------------------------------------------ # Main scan # ------------------------------------------------------------ $results = New-Object System.Collections.Generic.List[object] try { $sitesToScan = Resolve-SitesToScan -WebAppUrl $WebAppUrl -SiteCollectionUrl $SiteCollectionUrl Log-Message ("Resolved {0} site collection(s) for permission audit." -f $sitesToScan.Count) } catch { Add-ErrorRecord -Stage "ResolveSites" -Scope $WebAppUrl -ObjectTitle "Resolve-SitesToScan" -Message $_.Exception.Message throw } foreach ($site in $sitesToScan) { try { Log-Message ("Scanning site collection: {0}" -f $site.Url) foreach ($web in $site.AllWebs) { try { # ------------------------- # Web-level assignments # ------------------------- foreach ($assignment in $web.RoleAssignments) { try { $member = $assignment.Member $permissionNames = Get-PermissionNames -RoleAssignment $assignment $principalType = [string]$member.PrincipalType $memberCountEstimate = Get-MemberCountEstimate -Member $member $hasUniquePermissions = $false try { $hasUniquePermissions = [bool]$web.HasUniqueRoleAssignments } catch { } $exposureType = Get-ExposureType -PrincipalType $principalType -PermissionLevels $permissionNames $riskLevel = Get-RiskLevel -HasUniquePermissions $hasUniquePermissions -PermissionLevels $permissionNames -MemberCountEstimate $memberCountEstimate -ScopeType "Web" $results.Add([pscustomobject]@{ SiteCollectionUrl = $site.Url WebUrl = $web.Url ScopeType = "Web" ObjectTitle = $web.Title ObjectUrl = $web.Url HasUniquePermissions = $hasUniquePermissions PrincipalName = $member.Name PrincipalLogin = $member.LoginName PrincipalType = $principalType MemberCountEstimate = $memberCountEstimate PermissionLevels = $permissionNames ExposureType = $exposureType RiskLevel = $riskLevel Score = Get-Score -RiskLevel $riskLevel Category = "PermissionsAudit" ActionRecommendation = Get-Recommendation -ScopeType "Web" -RiskLevel $riskLevel -HasUniquePermissions $hasUniquePermissions }) | Out-Null } catch { Add-ErrorRecord -Stage "CollectWebAssignment" -Scope $web.Url -ObjectTitle $web.Title -Message $_.Exception.Message } } # ------------------------- # List / library assignments # ------------------------- foreach ($list in $web.Lists) { try { if (-not $list.HasUniqueRoleAssignments) { continue } foreach ($assignment in $list.RoleAssignments) { try { $member = $assignment.Member $permissionNames = Get-PermissionNames -RoleAssignment $assignment $principalType = [string]$member.PrincipalType $memberCountEstimate = Get-MemberCountEstimate -Member $member $exposureType = Get-ExposureType -PrincipalType $principalType -PermissionLevels $permissionNames $riskLevel = Get-RiskLevel -HasUniquePermissions $true -PermissionLevels $permissionNames -MemberCountEstimate $memberCountEstimate -ScopeType "ListOrLibrary" $objectUrl = "" try { $objectUrl = [string]$list.DefaultViewUrl } catch { } $results.Add([pscustomobject]@{ SiteCollectionUrl = $site.Url WebUrl = $web.Url ScopeType = "ListOrLibrary" ObjectTitle = $list.Title ObjectUrl = $objectUrl HasUniquePermissions = $true PrincipalName = $member.Name PrincipalLogin = $member.LoginName PrincipalType = $principalType MemberCountEstimate = $memberCountEstimate PermissionLevels = $permissionNames ExposureType = $exposureType RiskLevel = $riskLevel Score = Get-Score -RiskLevel $riskLevel Category = "PermissionsAudit" ActionRecommendation = Get-Recommendation -ScopeType "ListOrLibrary" -RiskLevel $riskLevel -HasUniquePermissions $true }) | Out-Null } catch { Add-ErrorRecord -Stage "CollectListAssignment" -Scope $web.Url -ObjectTitle $list.Title -Message $_.Exception.Message } } # ------------------------- # Optional item-level assignments # ------------------------- if ($IncludeItemLevel) { foreach ($item in $list.Items) { try { if (-not $item.HasUniqueRoleAssignments) { continue } foreach ($assignment in $item.RoleAssignments) { try { $member = $assignment.Member $permissionNames = Get-PermissionNames -RoleAssignment $assignment $principalType = [string]$member.PrincipalType $memberCountEstimate = Get-MemberCountEstimate -Member $member $exposureType = Get-ExposureType -PrincipalType $principalType -PermissionLevels $permissionNames $riskLevel = Get-RiskLevel -HasUniquePermissions $true -PermissionLevels $permissionNames -MemberCountEstimate $memberCountEstimate -ScopeType "Item" $itemTitle = "Item" try { if ($item.Name) { $itemTitle = [string]$item.Name } elseif ($item.Title) { $itemTitle = [string]$item.Title } } catch { } $itemUrl = "" try { $itemUrl = [string]$item.Url } catch { } $results.Add([pscustomobject]@{ SiteCollectionUrl = $site.Url WebUrl = $web.Url ScopeType = "Item" ObjectTitle = $itemTitle ObjectUrl = $itemUrl HasUniquePermissions = $true PrincipalName = $member.Name PrincipalLogin = $member.LoginName PrincipalType = $principalType MemberCountEstimate = $memberCountEstimate PermissionLevels = $permissionNames ExposureType = $exposureType RiskLevel = $riskLevel Score = Get-Score -RiskLevel $riskLevel Category = "PermissionsAudit" ActionRecommendation = Get-Recommendation -ScopeType "Item" -RiskLevel $riskLevel -HasUniquePermissions $true }) | Out-Null } catch { Add-ErrorRecord -Stage "CollectItemAssignment" -Scope $web.Url -ObjectTitle $list.Title -Message $_.Exception.Message } } } catch { Add-ErrorRecord -Stage "CollectItemPermissions" -Scope $web.Url -ObjectTitle $list.Title -Message $_.Exception.Message } } } } catch { Add-ErrorRecord -Stage "CollectListPermissions" -Scope $web.Url -ObjectTitle $list.Title -Message $_.Exception.Message } } } catch { Add-ErrorRecord -Stage "CollectWebPermissions" -Scope $site.Url -ObjectTitle $web.Url -Message $_.Exception.Message } finally { try { $web.Dispose() } catch { } } } try { $site.Dispose() } catch { } } catch { Add-ErrorRecord -Stage "CollectSitePermissions" -Scope $WebAppUrl -ObjectTitle $site.Url -Message $_.Exception.Message } } # ------------------------------------------------------------ # Export detail report # ------------------------------------------------------------ try { $results | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8 } catch { throw "Failed to export detail report. $($_.Exception.Message)" } # ------------------------------------------------------------ # Export summary report # ------------------------------------------------------------ try { $results | Group-Object RiskLevel, ScopeType | ForEach-Object { $parts = $_.Name -split ', ' [pscustomobject]@{ RiskLevel = $parts[0] ScopeType = $parts[1] Count = $_.Count } } | Export-Csv -Path $summaryPath -NoTypeInformation -Encoding UTF8 } catch { Add-ErrorRecord -Stage "ExportSummary" -Scope $summaryPath -ObjectTitle "SummaryExport" -Message $_.Exception.Message } # ------------------------------------------------------------ # Export run log # ------------------------------------------------------------ try { $runMessages | Set-Content -Path $logPath -Encoding UTF8 } catch { Add-ErrorRecord -Stage "ExportRunLog" -Scope $logPath -ObjectTitle "RunLog" -Message $_.Exception.Message } # ------------------------------------------------------------ # Export errors if needed # ------------------------------------------------------------ try { if ($errors.Count -gt 0) { $errors | Export-Csv -Path $errorPath -NoTypeInformation -Encoding UTF8 Write-Host ("ERROR REPORT: {0}" -f $errorPath) -ForegroundColor Yellow } } catch { Write-Host "Failed to export error report." -ForegroundColor Red } Write-Host "" Write-Host ("DETAIL REPORT: {0}" -f $OutputCsv) -ForegroundColor Green Write-Host ("SUMMARY REPORT: {0}" -f $summaryPath) -ForegroundColor Green Write-Host ("RUN LOG: {0}" -f $logPath) -ForegroundColor Green Write-Host "" Write-Host "Complete." -ForegroundColor Green SharePoint Production Toolkit - Script 06 - Permissions Audit Report .DESCRIPTION READ-ONLY. Scans a SharePoint web application or a single site collection and exports a permissions audit report. Captures: - Web-level permissions - Unique list/library permissions - Optional item-level unique permissions .PARAMETER WebAppUrl Target SharePoint Web Application URL. .PARAMETER OutputCsv Full path to the output CSV report. .PARAMETER SiteCollectionUrl Optional. Limit the scan to a single site collection. .PARAMETER IncludeItemLevel Optional. Includes item-level unique permissions. Can be slow in large environments. .PARAMETER NoPrompt Optional. Skips the confirmation prompt. .PARAMETER Remediate Included for toolkit consistency only. This script is READ-ONLY and does not change permissions. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$WebAppUrl,