Public/Report/Compare-SPCPermissionSnapshot.ps1

function Compare-SPCPermissionSnapshot {
    <#
    .SYNOPSIS
        Detects permission drift by comparing two permission snapshots.
 
    .DESCRIPTION
        This cmdlet compares a Baseline snapshot and a Current snapshot (in JSON format)
        to identify newly added Site Collection Administrators (SCA), Owners,
        direct Full Control assignments, and new guest accounts.
 
    .PARAMETER BaselineSnapshotPath
        Absolute path to the baseline snapshot file.
 
    .PARAMETER CurrentSnapshotPath
        Absolute path to the current snapshot file.
 
    .EXAMPLE
        Compare-SPCPermissionSnapshot -BaselineSnapshotPath "C:\snapshots\base.json" -CurrentSnapshotPath "C:\snapshots\current.json"
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "ERR-501: Baseline snapshot file not found." }
            return $true
        })]
        [string]$BaselineSnapshotPath,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "ERR-501: Current snapshot file not found." }
            return $true
        })]
        [string]$CurrentSnapshotPath
    )

    begin {
        Write-Verbose "Entering Compare-SPCPermissionSnapshot"
        try {
            Test-SPCConnection -ErrorAction Stop
        } catch {
            throw "ERR-001: Connection not found. Please connect using Connect-SPCTenant first."
        }
    }

    process {
        Write-Verbose "Loading Baseline Snapshot from $BaselineSnapshotPath"
        try {
            $baselineContent = Get-Content -Path $BaselineSnapshotPath -Raw -ErrorAction Stop
            $baselineData = ConvertFrom-Json -InputObject $baselineContent -ErrorAction Stop
        } catch {
            throw "ERR-502: Failed to parse Baseline snapshot. Ensure it is a valid JSON file."
        }

        Write-Verbose "Loading Current Snapshot from $CurrentSnapshotPath"
        try {
            $currentContent = Get-Content -Path $CurrentSnapshotPath -Raw -ErrorAction Stop
            $currentData = ConvertFrom-Json -InputObject $currentContent -ErrorAction Stop
        } catch {
            throw "ERR-502: Failed to parse Current snapshot. Ensure it is a valid JSON file."
        }

        # Initialize HashSets for quick lookups
        $baselineSCA = [System.Collections.Generic.HashSet[string]]::new()
        $baselineOwners = [System.Collections.Generic.HashSet[string]]::new()
        $baselineDirectFullControl = [System.Collections.Generic.HashSet[string]]::new()
        $baselineGuests = [System.Collections.Generic.HashSet[string]]::new()

        Write-Verbose "Processing Baseline Data..."
        foreach ($item in $baselineData) {
            $upn = $item.user.upn
            if ([string]::IsNullOrEmpty($upn)) { continue }

            if ($item.isSCA) { $baselineSCA.Add($upn) | Out-Null }
            if ($item.isOwner) { $baselineOwners.Add($upn) | Out-Null }
            if ($item.permissions -contains "Full Control") { $baselineDirectFullControl.Add($upn) | Out-Null }
            if ($upn -match "(?i)#EXT#") { $baselineGuests.Add($upn) | Out-Null }
        }

        # Lists for storing new findings
        $newSCA = [System.Collections.Generic.List[string]]::new()
        $newOwners = [System.Collections.Generic.List[string]]::new()
        $newDirectFullControl = [System.Collections.Generic.List[string]]::new()
        $newGuests = [System.Collections.Generic.List[string]]::new()

        Write-Verbose "Processing Current Data and detecting drift..."
        foreach ($item in $currentData) {
            $upn = $item.user.upn
            if ([string]::IsNullOrEmpty($upn)) { continue }

            if ($item.isSCA -and -not $baselineSCA.Contains($upn)) {
                $newSCA.Add($upn)
            }
            if ($item.isOwner -and -not $baselineOwners.Contains($upn)) {
                $newOwners.Add($upn)
            }
            if ($item.permissions -contains "Full Control" -and -not $baselineDirectFullControl.Contains($upn)) {
                $newDirectFullControl.Add($upn)
            }
            if (($upn -match "(?i)#EXT#") -and -not $baselineGuests.Contains($upn)) {
                $newGuests.Add($upn)
            }
        }

        $driftAlert = [PSCustomObject]@{
            NewSCAOrOwners                 = @($newSCA) + @($newOwners) | Select-Object -Unique
            NewDirectFullControlAssignments = $newDirectFullControl.ToArray()
            NewGuestAccounts               = $newGuests.ToArray()
        }

        return $driftAlert
    }

    end {
        Write-Information "Permission snapshot comparison completed successfully."
    }
}