Src/Public/Invoke-AsBuiltReport.Microsoft.SharePoint.ps1
|
function Invoke-AsBuiltReport.Microsoft.SharePoint { <# .SYNOPSIS PowerShell script to document the configuration of Microsoft SharePoint Online and OneDrive for Business in Word/HTML/Text formats. .DESCRIPTION Documents the configuration of SharePoint Online and OneDrive for Business in Word/HTML/Text formats using PScribo. Covers: - Tenant Overview (identity, licensing, storage quota) - Sharing Policy (external sharing levels, Anyone links, domain restrictions) - External Access Controls (unmanaged devices, idle session, IP restrictions) - Site Collections (inventory, storage, owner, inactivity) - OneDrive for Business (quotas, sync restrictions, per-user summary) - Compliance & Data Governance (audit log, DLP, retention, sensitivity labels) Compliance Frameworks: - ACSC Essential Eight (ML1-ML3) -- SharePoint/OneDrive controls - CIS Microsoft 365 Foundations Benchmark v6.0.1 -- Chapter 7 controls Excel Export: After report generation, an Excel workbook is automatically exported containing sheets for all major data sets for audit and remediation tracking. .NOTES Version: 0.1.0 Author: Pai Wei Sing Github: weising26 .LINK https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.SharePoint .PARAMETER Target Specifies the Microsoft 365 tenant domain name (e.g. contoso.onmicrosoft.com). .PARAMETER Credential Optional PSCredential. The username is used as the UPN if Options.UserPrincipalName is not set in the report config JSON. #> param ( [String[]] $Target, [PSCredential] $Credential ) Write-ReportModuleInfo -ModuleName 'Microsoft.SharePoint' #---------------------------------------------------------------------------------------------# # Dependency Module Version Check # #---------------------------------------------------------------------------------------------# $ModuleArray = @('AsBuiltReport.Core', 'Microsoft.Graph', 'PnP.PowerShell', 'ImportExcel') foreach ($Module in $ModuleArray) { try { $InstalledVersion = Get-Module -ListAvailable -Name $Module -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version if ($InstalledVersion) { Write-Host " - $Module module v$($InstalledVersion.ToString()) is currently installed." $PSGetAvailable = Get-Module -ListAvailable -Name PowerShellGet -ErrorAction SilentlyContinue if ($PSGetAvailable) { try { Import-Module PowerShellGet -ErrorAction SilentlyContinue $LatestVersion = Find-Module -Name $Module -Repository PSGallery -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Version if ($LatestVersion -and $InstalledVersion -lt $LatestVersion) { Write-Host " - $Module module v$($LatestVersion.ToString()) is available." -ForegroundColor Red Write-Host " - Run 'Update-Module -Name $Module -Force' to install the latest version." -ForegroundColor Red } } catch { Write-PScriboMessage -IsWarning "Unable to check latest version for ${Module}: $($_.Exception.Message)" } } } else { $OptionalModules = @('ImportExcel', 'PnP.PowerShell') $IsOptional = $OptionalModules -contains $Module $Required = if ($Module -eq 'ImportExcel') { '(Optional -- required for Excel export)' } ` elseif ($Module -eq 'PnP.PowerShell') { '(Optional but highly recommended -- required for SharePoint tenant settings and site collection data)' } ` else { '' } $Color = if ($IsOptional) { 'Yellow' } else { 'Red' } Write-Host " - $Module module is NOT installed. $Required Run 'Install-Module -Name $Module -Force'." -ForegroundColor $Color } } catch { Write-PScriboMessage -IsWarning $_.Exception.Message } } #---------------------------------------------------------------------------------------------# # PRE-FLIGHT: Import Graph sub-modules # #---------------------------------------------------------------------------------------------# $script:RequiredGraphModules = @( 'Microsoft.Graph.Authentication' 'Microsoft.Graph.Identity.DirectoryManagement' 'Microsoft.Graph.Users' ) Write-Host " - Pre-loading Microsoft Graph sub-modules..." -ForegroundColor Cyan foreach ($GraphModule in $script:RequiredGraphModules) { if (Get-Module -Name $GraphModule -ErrorAction SilentlyContinue) { Write-Host " - Already loaded: $GraphModule" -ForegroundColor DarkGray continue } try { Import-Module $GraphModule -Global -ErrorAction Stop Write-Host " - Loaded: $GraphModule" -ForegroundColor DarkGray } catch { Write-Warning " [!] Could not load ${GraphModule}: $($_.Exception.Message)" } } #---------------------------------------------------------------------------------------------# # Import Report Configuration # #---------------------------------------------------------------------------------------------# $ReportConfig.Report.Name = 'Microsoft SharePoint & OneDrive As Built Report' $script:Report = $ReportConfig.Report $script:InfoLevel = $ReportConfig.InfoLevel $script:Options = $ReportConfig.Options $script:TextInfo = (Get-Culture).TextInfo $script:SectionTimers = [System.Collections.Generic.Dictionary[string, object]]::new() # Compliance framework toggles $script:IncludeACSCe8 = ($script:Options.ComplianceFrameworks.ACSCe8 -eq $true) $script:IncludeCISBaseline = ($script:Options.ComplianceFrameworks.CISBaseline -eq $true) if ($script:IncludeACSCe8) { Write-Host " - Compliance framework: ACSC Essential Eight enabled" -ForegroundColor Cyan } if ($script:IncludeCISBaseline) { Write-Host " - Compliance framework: CIS Baseline enabled" -ForegroundColor Cyan } if (-not $script:IncludeACSCe8 -and -not $script:IncludeCISBaseline) { Write-Host " - Compliance frameworks: none enabled (set Options.ComplianceFrameworks in JSON to enable)" -ForegroundColor DarkGray } # Load compliance check definitions from JSON Initialize-AbrSPComplianceFrameworks # Excel export sheet collector $script:ExcelSheets = [System.Collections.Specialized.OrderedDictionary]::new() $script:E8AllChecks = [System.Collections.ArrayList]::new() $script:CISAllChecks = [System.Collections.ArrayList]::new() #---------------------------------------------------------------------------------------------# # Transcript Log Setup # #---------------------------------------------------------------------------------------------# $script:TranscriptOutputFolder = $null if ($ReportConfig.Report.OutputFolderPath -and (Test-Path $ReportConfig.Report.OutputFolderPath -ErrorAction SilentlyContinue)) { $script:TranscriptOutputFolder = $ReportConfig.Report.OutputFolderPath } elseif ($OutputFolderPath -and (Test-Path $OutputFolderPath -ErrorAction SilentlyContinue)) { $script:TranscriptOutputFolder = $OutputFolderPath } else { $script:TranscriptOutputFolder = Join-Path $env:USERPROFILE 'Documents' } $TranscriptTimestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $script:TranscriptLogPath = Join-Path $script:TranscriptOutputFolder "SharePoint_Transcript_${TranscriptTimestamp}.log" $script:ErrorLogPath = Join-Path $script:TranscriptOutputFolder "SharePoint_Errors_${TranscriptTimestamp}.log" try { Start-Transcript -Path $script:TranscriptLogPath -Append -ErrorAction Stop | Out-Null Write-Host " - Transcript log: $script:TranscriptLogPath" -ForegroundColor Cyan } catch { Write-Warning " - Could not start transcript: $($_.Exception.Message)" $script:TranscriptLogPath = $null } $script:ErrorLog = [System.Collections.Generic.List[string]]::new() #---------------------------------------------------------------------------------------------# # Debug Logger Setup # #---------------------------------------------------------------------------------------------# $script:DebugLogEnabled = ($script:Options.DebugLog -eq $true) $script:DebugLogEntries = [System.Collections.Generic.List[string]]::new() #---------------------------------------------------------------------------------------------# # Disclaimer (HealthCheck) # #---------------------------------------------------------------------------------------------# if ($Healthcheck) { Section -Style TOC -ExcludeFromTOC 'DISCLAIMER' { Paragraph 'This report was generated using the AsBuiltReport framework. Health check indicators are provided as guidance only and should be validated by a qualified administrator before remediation. Colour coding: RED = Critical issue requiring immediate attention, YELLOW/AMBER = Warning requiring review.' } PageBreak } #---------------------------------------------------------------------------------------------# # Connection Section # #---------------------------------------------------------------------------------------------# foreach ($System in $Target) { Write-Host " " Write-Host "- Starting SharePoint report for tenant: $System" Write-TranscriptLog "Starting SharePoint report for tenant: $System" 'INFO' 'MAIN' #region Resolve UPN $ResolvedUPN = $null if ($Options.UserPrincipalName -and (Test-UserPrincipalName -UserPrincipalName $Options.UserPrincipalName)) { $ResolvedUPN = $Options.UserPrincipalName } elseif ($Credential -and (Test-UserPrincipalName -UserPrincipalName $Credential.UserName)) { $ResolvedUPN = $Credential.UserName } else { throw "No valid UserPrincipalName found. Set 'Options.UserPrincipalName' in your report config JSON (e.g. admin@$System), or pass -Credential with a UPN-format username." } Write-TranscriptLog "Using UPN: $ResolvedUPN" 'INFO' 'AUTH' #endregion #region Derive SharePoint Admin URL # e.g. contoso.onmicrosoft.com -> https://contoso-admin.sharepoint.com $TenantPrefix = ($System -split '\.')[0] $script:TenantAdminUrl = "https://${TenantPrefix}-admin.sharepoint.com" $script:TenantRootUrl = "https://${TenantPrefix}.sharepoint.com" if ($Options.TenantAdminUrl -and $Options.TenantAdminUrl -ne '') { $script:TenantAdminUrl = $Options.TenantAdminUrl } Write-Host " - SharePoint Admin URL: $($script:TenantAdminUrl)" -ForegroundColor Cyan #endregion #region Connect try { Connect-SharePointSession -UserPrincipalName $ResolvedUPN -TenantAdminUrl $script:TenantAdminUrl } catch { Write-AbrDebugLog "Connection failed: $($_.Exception.Message)" 'ERROR' 'AUTH' throw "Connection failed for tenant '$System'. Error: $($_.Exception.Message)" } #endregion #region Retrieve Tenant Info # Graph data was pre-fetched in Connect-SharePointSession BEFORE PnP connected. # PnP.PowerShell v3 loads Graph.Core v1.25.1 which breaks Graph SDK after connecting. # $script:CachedOrg is populated before that happens. Write-Host " - Retrieving tenant information from pre-fetched cache..." try { $MgOrg = $script:CachedOrg if (-not $MgOrg) { throw "Organisation cache is empty -- Graph pre-fetch failed during connection." } $script:TenantId = $MgOrg.id $script:TenantDomain = ($MgOrg.verifiedDomains | Where-Object { $_.isDefault }).name if (-not $script:TenantDomain) { $script:TenantDomain = $System } $script:TenantName = if ($MgOrg.displayName) { $MgOrg.displayName } else { $script:TenantDomain } Write-Host " - Tenant: $($script:TenantName) / $($script:TenantDomain) ($($script:TenantId))" -ForegroundColor Cyan Write-TranscriptLog "Tenant: $($script:TenantName) / $($script:TenantDomain) ($($script:TenantId))" 'INFO' 'MAIN' } catch { $script:TenantId = $System $script:TenantDomain = $System $script:TenantName = $System Write-TranscriptLog "Unable to retrieve tenant info from cache: $($_.Exception.Message)" 'WARNING' 'MAIN' } #endregion #---------------------------------------------------------------------------------------------# # Report Sections # #---------------------------------------------------------------------------------------------# if (-not $script:TenantName) { $script:TenantName = $System } if (-not $script:TenantDomain) { $script:TenantDomain = $System } # Tenant Overview if ($InfoLevel.TenantOverview -ge 1) { Write-Host '- Working on Tenant Overview section.' Write-AbrDebugLog 'Starting TenantOverview section' 'DEBUG' 'SECTION' Get-AbrSPTenantOverview -TenantId $script:TenantName } # Sharing Policy + External Access if ($InfoLevel.SharingPolicy -ge 1 -or $InfoLevel.ExternalAccess -ge 1) { Write-Host '- Working on Sharing & External Access section.' Write-AbrDebugLog 'Starting Sharing section' 'DEBUG' 'SECTION' Get-AbrSPSharingSection -TenantId $script:TenantName } # Site Collections + OneDrive if ($InfoLevel.SiteCollections -ge 1 -or $InfoLevel.OneDrive -ge 1) { Write-Host '- Working on Content & Storage section.' Write-AbrDebugLog 'Starting Content section' 'DEBUG' 'SECTION' Get-AbrSPContentSection -TenantId $script:TenantName } # Compliance if ($InfoLevel.Compliance -ge 1) { Write-Host '- Working on Compliance section.' Write-AbrDebugLog 'Starting Compliance section' 'DEBUG' 'SECTION' Get-AbrSPComplianceSection -TenantId $script:TenantName } # Security Posture & Advisory (runs last -- consumes accumulated compliance check data) Write-AbrDebugLog 'Starting SecurityPosture section' 'DEBUG' 'SECTION' Get-AbrSPSecurityPostureSection -TenantId $script:TenantName #---------------------------------------------------------------------------------------------# # Output Folder # #---------------------------------------------------------------------------------------------# if ($ReportConfig.Report.OutputFolderPath -and (Test-Path $ReportConfig.Report.OutputFolderPath -ErrorAction SilentlyContinue)) { $script:ResolvedOutputFolder = $ReportConfig.Report.OutputFolderPath } elseif ($OutputFolderPath -and (Test-Path $OutputFolderPath -ErrorAction SilentlyContinue)) { $script:ResolvedOutputFolder = $OutputFolderPath } else { $script:ResolvedOutputFolder = Join-Path $env:USERPROFILE 'Documents' } Write-Host " - Output folder: $($script:ResolvedOutputFolder)" -ForegroundColor Cyan #---------------------------------------------------------------------------------------------# # Excel Export # #---------------------------------------------------------------------------------------------# # Consolidate compliance sheets if ($script:E8AllChecks -and $script:E8AllChecks.Count -gt 0) { $script:ExcelSheets['ACSC E8 Assessment'] = $script:E8AllChecks } if ($script:CISAllChecks -and $script:CISAllChecks.Count -gt 0) { $script:ExcelSheets['CIS Assessment'] = $script:CISAllChecks } if ($script:ExcelSheets -and $script:ExcelSheets.Count -gt 0) { Write-Host " " Write-Host "- Exporting data to Excel workbook..." Write-AbrDebugLog "Exporting $($script:ExcelSheets.Count) sheets to Excel" 'INFO' 'EXPORT' $ExcelOutputPath = Join-Path $script:ResolvedOutputFolder ` "SharePoint_AsBuilt_$($script:TenantDomain)_$(Get-Date -Format 'yyyyMMdd_HHmmss').xlsx" try { Export-SharePointToExcel -Sheets $script:ExcelSheets -Path $ExcelOutputPath -TenantId $script:TenantName Write-AbrDebugLog "Excel export complete: $ExcelOutputPath" 'SUCCESS' 'EXPORT' } catch { Write-Warning "Excel export failed: $($_.Exception.Message)" Write-AbrDebugLog "Excel export failed: $($_.Exception.Message)" 'ERROR' 'EXPORT' } } #---------------------------------------------------------------------------------------------# # Clean Up Connections # #---------------------------------------------------------------------------------------------# Write-Host " " Write-Host "- Finished report generation for tenant: $($script:TenantName)" Write-TranscriptLog "Report generation complete for: $($script:TenantName)" 'SUCCESS' 'MAIN' if ($script:Options.KeepSession -eq $true) { Write-Host " - KeepSession is enabled -- sessions retained." -ForegroundColor Yellow } else { Disconnect-SharePointSession } #---------------------------------------------------------------------------------------------# # Debug Log Export # #---------------------------------------------------------------------------------------------# if ($script:DebugLogEnabled -and $script:DebugLogEntries.Count -gt 0) { $script:DebugLogPath = Join-Path $script:ResolvedOutputFolder ` "SharePoint_DebugLog_$($script:TenantDomain)_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" try { $script:DebugLogEntries | Set-Content -Path $script:DebugLogPath -Encoding UTF8 Write-Host " - Debug log saved to: $($script:DebugLogPath)" -ForegroundColor Cyan } catch { Write-Warning "Could not save debug log: $($_.Exception.Message)" } } if ($script:TranscriptLogPath) { try { Stop-Transcript -ErrorAction SilentlyContinue | Out-Null Write-Host " - Transcript saved to: $script:TranscriptLogPath" -ForegroundColor Cyan } catch {} } } #endregion foreach Target loop } |