Public/Report/Invoke-SPCPermissionAnalytics.ps1

function Invoke-SPCPermissionAnalytics {
    <#
    .SYNOPSIS
        Performs tenant-wide permission analytics and exports structured metrics and health score.
    .DESCRIPTION
        Aggregates orphaned users, external guest risks, privileged/over-permissioned accounts,
        broken inheritance, and unowned sites to calculate a global Permission Health Score.
    .PARAMETER OutputPath
        Target JSON output file path. Defaults to .\Analytics.json.
    .PARAMETER BrokenInheritanceThreshold
        Threshold of unique broken permission scopes to flag a site. Defaults to 1000.
    .OUTPUTS
        [PSCustomObject]
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$OutputPath = ".\Analytics.json",

        [Parameter(Mandatory = $false)]
        [int]$BrokenInheritanceThreshold = 1000
    )

    begin {
        Write-Verbose "Starting Invoke-SPCPermissionAnalytics"
        Test-SPCConnection
        $analyticsResult = $null
    }

    process {
        Write-Verbose "Running scans for Analytics..."
        $orphans = @(Get-SPCOrphanedUser -AllSites -IncludeGuests -IncludeDisabled)
        $OrphanedUserCount = $orphans.Count

        $guests = @(Get-SPCGuestAccess -ErrorAction SilentlyContinue)
        $HighRiskGuestCount = @($guests | Where-Object { $_.RiskLevel -eq 'HIGH' }).Count

        $overPermissioned = @(Get-SPCOverPermissionedUser -ErrorAction SilentlyContinue)
        $OverPermissionedUserCount = $overPermissioned.Count

        $tenantSites = Get-PnPTenantSite -Connection $script:SPCContext.PnPContext -ErrorAction SilentlyContinue
        
        $BrokenInheritanceSiteCount = 0
        $MissingOwnerSiteCount      = 0

        foreach ($site in $tenantSites) {
            if ($site.Template -like 'REDIRECTSITE#*') { continue }
            
            # Missing Owner check
            if ([string]::IsNullOrWhiteSpace($site.Owner)) {
                $MissingOwnerSiteCount++
            }

            # Broken inheritance check
            try {
                $broken = Get-SPCBrokenInheritance -SiteUrl $site.Url -ErrorAction SilentlyContinue
                if ($broken.UniqueScopes -gt $BrokenInheritanceThreshold) {
                    $BrokenInheritanceSiteCount++
                }
            } catch {
                # Ignore errors on individual sites
            }
        }

        # Calculate Score using approved Measure-SPCScoreInternal
        $scoreResult = Measure-SPCScoreInternal `
            -OrphanedUserCount $OrphanedUserCount `
            -HighRiskGuestCount $HighRiskGuestCount `
            -OverPermissionedUserCount $OverPermissionedUserCount `
            -BrokenInheritanceSiteCount $BrokenInheritanceSiteCount `
            -MissingOwnerSiteCount $MissingOwnerSiteCount

        $analyticsResult = [PSCustomObject]@{
            TenantName  = $script:SPCContext.TenantName
            GeneratedAt = (Get-Date -Format 'o')
            Metrics     = [PSCustomObject]@{
                OrphanedUserCount          = $OrphanedUserCount
                HighRiskGuestCount         = $HighRiskGuestCount
                OverPermissionedUserCount  = $OverPermissionedUserCount
                BrokenInheritanceSiteCount = $BrokenInheritanceSiteCount
                MissingOwnerSiteCount      = $MissingOwnerSiteCount
            }
            HealthScore = $scoreResult
        }

        $analyticsResult | ConvertTo-Json -Depth 5 | Set-Content -Path $OutputPath -Encoding UTF8 -Force
        
        Write-Verbose "Analytics JSON generated at $OutputPath"
    }

    end {
        Write-Information "Analytics generation completed successfully. Output saved to $OutputPath" -InformationAction Continue
        return $analyticsResult
    }
}