Tools/Get-SPOData.ps1

###################################################################################################
# Script: Get-SPOData.ps1
# Author: Ryan Holderread - Rackspace Technology
# Version: 2.0
# Last Updated: 2026-03-25
#
# Description:
# Companion script to the M365-QuickAssess module.
# Runs in Windows PowerShell 5.1 (required by SharePoint Online Management Shell).
# Spawned automatically by Get-SharePointData.ps1 -- do not run directly.
#
# Collects SharePoint site metrics and outputs a structured JSON file
# that is merged back into the main assessment by the parent process.
#
# Parameters:
# -OutputFile Full path to the JSON output file
# -AdminUrl SharePoint admin URL (e.g. https://contoso-admin.sharepoint.com)
###################################################################################################

param
(
    [Parameter(Mandatory)]
    [string]$OutputFile,

    [Parameter(Mandatory)]
    [string]$AdminUrl
)

$ErrorActionPreference = "Stop"

# -------------------------------------------------------------------
# Logging (console only -- parent process owns the log file)
# -------------------------------------------------------------------
function Write-Log
{
    param ( [string]$Message, [string]$Level = "INFO" )

    $timestamp = ( Get-Date ).ToString("yyyy-MM-dd HH:mm:ss")
    $entry     = "[$timestamp][$Level] $Message"

    switch ( $Level )
    {
        "ERROR" { Write-Host $entry -ForegroundColor Red    }
        "WARN"  { Write-Host $entry -ForegroundColor Yellow }
        default { Write-Host $entry }
    }
}

# -------------------------------------------------------------------
# Module
# -------------------------------------------------------------------
try
{
    Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction Stop
}
catch
{
    Write-Log "Failed to import SharePoint Online Management Shell: $( $_.Exception.Message )" "ERROR"
    Write-Log "Install it with: Install-Module Microsoft.Online.SharePoint.PowerShell"
    exit 1
}

# -------------------------------------------------------------------
# Connection
# -------------------------------------------------------------------
try
{
    Write-Log "Connecting to SharePoint Online: $AdminUrl"
    Connect-SPOService -Url $AdminUrl -UseSystemBrowser:$true
    Write-Log "Connected to SharePoint Online"
}
catch
{
    Write-Log "SharePoint connection failed: $( $_.Exception.Message )" "ERROR"
    exit 1
}

# -------------------------------------------------------------------
# Data Collection
# -------------------------------------------------------------------
$Findings = @()

$siteCount       = 0
$totalStorageGB  = 0
$largeSites      = 0
$externalSharing = $false
$uniquePerms     = $false
$hasAppCatalog   = $false
$customAppCount  = 0

try
{
    Write-Log "Collecting SharePoint site data"

    $sites = Get-SPOSite -Limit All -ErrorAction Stop

    $siteCount      = $sites.Count
    $totalStorageGB = [math]::Round( ( $sites | Measure-Object StorageUsageCurrent -Sum ).Sum / 1024, 2 )

    Write-Log "Sites: $siteCount TotalStorageGB: $totalStorageGB"

    # -------------------------------------------------------------------
    # Large Sites (> 100 GB)
    # -------------------------------------------------------------------
    $largeSiteObjects = $sites | Where-Object { $_.StorageUsageCurrent -gt 102400 }
    $largeSites       = $largeSiteObjects.Count

    foreach ( $site in $largeSiteObjects )
    {
        $sizeGB = [math]::Round( $site.StorageUsageCurrent / 1024, 2 )

        $Findings += @{
            Type   = "LargeSharePointSite"
            Url    = $site.Url
            Title  = $site.Title
            SizeGB = $sizeGB
        }
    }

    # -------------------------------------------------------------------
    # External Sharing
    # -------------------------------------------------------------------
    $externalSharing = ( $sites | Where-Object {
        $_.SharingCapability -ne "Disabled"
    } ).Count -gt 0

    if ( $externalSharing )
    {
        $externalSites = $sites | Where-Object { $_.SharingCapability -ne "Disabled" }

        foreach ( $site in $externalSites | Select-Object -First 10 )
        {
            $Findings += @{
                Type = "ExternalSharing"
                Url  = $site.Url
            }
        }
    }

    # -------------------------------------------------------------------
    # Unique Permissions (signal only -- details require full discovery)
    # -------------------------------------------------------------------
    $uniquePerms = ( $sites | Where-Object {
        $_.HasUniqueRoleAssignments -eq $true
    } ).Count -gt 0

    # -------------------------------------------------------------------
    # App Catalog
    # -------------------------------------------------------------------
    try
    {
        $appCatalogSite = $sites | Where-Object {
            $_.Url -match "/sites/appcatalog" -or
            $_.Template -eq "APPCATALOG#0"
        } | Select-Object -First 1

        if ( $appCatalogSite )
        {
            $hasAppCatalog = $true

            Write-Log "App Catalog found: $( $appCatalogSite.Url )"

            try
            {
                $appCatalogApps = Get-SPOAppInfo -ProductId * -ErrorAction Stop
                $customAppCount = ( $appCatalogApps | Measure-Object ).Count
                Write-Log "Apps in catalog: $customAppCount (uploaded, not necessarily deployed)"
            }
            catch
            {
                Write-Log "Could not retrieve app catalog contents: $( $_.Exception.Message )" "WARN"
                $customAppCount = 0
            }
        }
        else
        {
            Write-Log "No App Catalog detected"
        }
    }
    catch
    {
        Write-Log "App Catalog check failed: $( $_.Exception.Message )" "WARN"
    }
}
catch
{
    Write-Log "SharePoint data collection failed: $( $_.Exception.Message )" "ERROR"
    exit 1
}

# -------------------------------------------------------------------
# Storage Finding
# -------------------------------------------------------------------
if ( $totalStorageGB -gt 100 )
{
    $severity = if ( $totalStorageGB -gt 500 ) { "High" } else { "Medium" }

    $significantSites = $sites | Where-Object { $_.StorageUsageCurrent -gt 102400 }

    $siteList = ( $significantSites | ForEach-Object {
        "$( $_.Title ) - $( $_.Url ) ($( [math]::Round( $_.StorageUsageCurrent / 1024, 2 ) ) GB)"
    } ) -join ", "

    if ( $siteList -eq "" )
    {
        $siteList = "No sites over 100 GB detected"
    }

    $Findings += @{
        Type        = "StorageUsage"
        Severity    = $severity
        Description = "$totalStorageGB GB total across $siteCount sites: $siteList"
    }
}

# -------------------------------------------------------------------
# Unique Permissions Finding
# -------------------------------------------------------------------
if ( $uniquePerms )
{
    $Findings += @{
        Type = "UniquePermissions"
    }
}

# -------------------------------------------------------------------
# Build Output
# -------------------------------------------------------------------
$result = [ordered]@{
    SharePoint = [ordered]@{
        SharePointSiteCount            = $siteCount
        SharePointTotalStorageGB       = $totalStorageGB
        LargeSiteCount                 = $largeSites
        HasExternalSharing             = $externalSharing
        HasUniquePermissions           = $uniquePerms
        HasSharePointRetentionPolicies = $false
        HasAppCatalog                  = $hasAppCatalog
        HasSharePointCustomApps        = ( $customAppCount -gt 0 )
        SharePointCustomAppCount       = $customAppCount
    }

    Summary = [ordered]@{
        SharePointSiteCount      = $siteCount
        SharePointTotalStorageGB = $totalStorageGB
    }

    Findings = $Findings
}

# -------------------------------------------------------------------
# Write Output
# -------------------------------------------------------------------
try
{
    Write-Log "Writing SharePoint output to $OutputFile"
    $result | ConvertTo-Json -Depth 6 | Out-File $OutputFile -Encoding utf8
    Write-Log "SharePoint export complete"
    exit 0
}
catch
{
    Write-Log "Failed to write output file: $( $_.Exception.Message )" "ERROR"
    exit 1
}