Public/Get-TntAzureSecureScoreReport.ps1

function Get-TntAzureSecureScoreReport {
    <#
    .SYNOPSIS
        Retrieves Azure Secure Score data from all subscriptions within the tenant.

    .DESCRIPTION
        This function connects to Azure Resource Manager using an app registration and retrieves
        Azure Security Center secure scores from all accessible subscriptions. It provides comprehensive
        reporting on security posture, recommendations, and compliance across the Azure environment.

        in PowerShell scripts.

    .PARAMETER TenantId
        The Azure AD Tenant ID (GUID) to connect to.

    .PARAMETER ClientId
        The Application (Client) ID of the app registration created for security reporting.

    .PARAMETER ClientSecret
        The client secret for the app registration. Accepts SecureString or plain String.

    .PARAMETER CertificateThumbprint
        The thumbprint of the certificate to use for authentication instead of client secret.

    .PARAMETER IncludeRecommendations
        Switch to include detailed security recommendations for each subscription.

    .PARAMETER FilterBySubscription
        Filter results to specific subscription IDs. Accepts array of subscription IDs.

    .PARAMETER MaxConcurrentRequests
        Maximum number of concurrent API requests. Defaults to 5 for rate limiting.

    .PARAMETER IncludeComplianceScore
        Switch to include regulatory compliance scores where available.

    .PARAMETER IncludeHistoricalData
        Switch to include historical secure score data for trend analysis (last 90 days).

    .PARAMETER MaxHistoryDays
        Maximum number of days of historical data to retrieve. Defaults to 90 days.

    .EXAMPLE
        Get-TntAzureSecureScoreReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret

        Retrieves Azure Secure Score data from all accessible subscriptions.

    .EXAMPLE
        Get-TntAzureSecureScoreReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret |
            ConvertTo-Json -Depth 10 | Out-File -Path 'AzureSecureScore.json'

        Exports the report to JSON format.

    .EXAMPLE
        $Report = Get-TntAzureSecureScoreReport @params -IncludeRecommendations -IncludeHistoricalData
        $Report.SubscriptionScores | Sort-Object ScorePercentage | Format-Table

        Retrieves comprehensive data and displays subscription scores.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns a structured report object containing:
        - Summary: Aggregated statistics across all subscriptions
        - SubscriptionScores: Per-subscription secure score data
        - ComplianceData: Regulatory compliance scores (if -IncludeComplianceScore specified)
        - TrendAnalysis: Historical trend analysis (if -IncludeHistoricalData specified)
        - HistoricalScores: Raw historical data (if -IncludeHistoricalData specified)
        - Recommendations: Security recommendations (if -IncludeRecommendations specified)
        - ProcessingErrors: Any errors encountered during processing

    .NOTES
        Author: Tom de Leeuw
        Website: https://systom.dev
        Module: TenantReports

        Required Permissions:
        - Microsoft Graph: Directory.Read.All (Application)
        - Azure Service Management: user_impersonation (Delegated)

        Additional Requirements:
        - Security Reader role on each subscription (or Contributor/Reader role)

    .LINK
        https://systom.dev
    #>


    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param(
        # Tenant ID of the Microsoft 365 tenant.
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [string]$TenantId,

        # Application (client) ID of the registered app.
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(ParameterSetName = 'Interactive')]
        [Alias('ApplicationId')]
        [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')]
        [string]$ClientId,

        # Client secret credential when using secret-based authentication.
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Alias('ApplicationSecret')]
        [ValidateNotNullOrEmpty()]
        [SecureString]$ClientSecret,

        # Certificate thumbprint for certificate-based authentication.
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [ValidateNotNullOrEmpty()]
        [string]$CertificateThumbprint,

        # Use interactive authentication (no app registration required).
        [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')]
        [switch]$Interactive,

        # Switch to include detailed security recommendations.
        [Parameter()]
        [switch]$IncludeRecommendations,

        # Optional list of subscription IDs to scope results.
        [Parameter()]
        [string[]]$FilterBySubscription,

        # Maximum number of concurrent API calls.
        [Parameter()]
        [ValidateRange(1, 10)]
        [int]$MaxConcurrentRequests = 5,

        # Switch to include regulatory compliance scores.
        [Parameter()]
        [switch]$IncludeComplianceScore,

        # Switch to include historical trend data.
        [Parameter()]
        [switch]$IncludeHistoricalData,

        # Maximum number of days of historical data to retrieve.
        [Parameter()]
        [ValidateRange(1, 365)]
        [int]$MaxHistoryDays = 90
    )

    begin {
        # Azure Resource Manager endpoint
        $Script:ArmBaseUri = 'https://management.azure.com'

        Write-Information 'Starting Azure Secure Score collection across subscriptions...' -InformationAction Continue
    }

    process {
        try {
            # Establish connection
            $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters
            $ConnectionParams['Scope'] = 'Azure'
            $ConnectionParams['ConnectionType'] = 'RestApi'
            $ConnectionInfo = Connect-TntGraphSession @ConnectionParams
            $Script:ArmHeaders = $ConnectionInfo.Headers

            $SubscriptionsUri      = "$($Script:ArmBaseUri)/subscriptions?api-version=2020-01-01"
            $SubscriptionsResponse = Invoke-RestMethod -Uri $SubscriptionsUri -Headers $Script:ArmHeaders -Method GET -ErrorAction Stop
            $AllSubscriptions      = $SubscriptionsResponse.value | Where-Object { $_.state -eq 'Enabled' }

            # Filter subscriptions if specified
            if ($FilterBySubscription) {
                $AllSubscriptions = $AllSubscriptions | Where-Object { $_.subscriptionId -in $FilterBySubscription }
            }

            Write-Verbose "Found $($AllSubscriptions.Count) enabled subscriptions to process"

            if ($AllSubscriptions.Count -eq 0) {
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new('Get-TntAzureSecureScoreReport failed: No enabled subscriptions found or insufficient permissions.'),
                    'GetAzureSecureScoreReportNoSubscriptionsError',
                    [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                    $TenantId
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            # Process subscriptions for secure score data
            $SubscriptionSecureScores = [System.Collections.Generic.List[PSObject]]::new()
            $OverallRecommendations   = [System.Collections.Generic.List[PSObject]]::new()
            $ComplianceData           = [System.Collections.Generic.List[PSObject]]::new()
            $ProcessingErrors         = [System.Collections.Generic.List[PSObject]]::new()

            foreach ($Subscription in $AllSubscriptions) {

                try {
                    # Get secure score for subscription
                    Write-Verbose "Processing subscription: $($Subscription.displayName) ($($Subscription.subscriptionId))"

                    $SecureScoreUri = "$($Script:ArmBaseUri)/subscriptions/$($Subscription.subscriptionId)/providers/Microsoft.Security/secureScores/ascScore?api-version=2020-01-01"

                    try {
                        $SecureScoreResponse = Invoke-RestMethod -Uri $SecureScoreUri -Headers $Script:ArmHeaders -Method GET -ErrorAction Stop

                        # Get secure score controls for detailed breakdown
                        $ControlsUri      = "$($Script:ArmBaseUri)/subscriptions/$($Subscription.subscriptionId)/providers/Microsoft.Security/secureScoreControls?api-version=2020-01-01"
                        $ControlsResponse = Invoke-RestMethod -Uri $ControlsUri -Headers $Script:ArmHeaders -Method GET -ErrorAction SilentlyContinue

                        # Calculate control statistics using single-pass
                        $Controls              = $ControlsResponse.value ?? @()
                        $HealthyControls       = 0
                        $UnhealthyControls     = 0
                        $NotApplicableControls = 0
                        foreach ($CtrlItem in $Controls) {
                            if ($CtrlItem.properties.score.max -eq 0) {
                                $NotApplicableControls++
                            } elseif ($CtrlItem.properties.score.current -eq $CtrlItem.properties.score.max) {
                                $HealthyControls++
                            } else {
                                $UnhealthyControls++
                            }
                        }

                        # Collect recommendations for this subscription if requested
                        $SubscriptionRecommendations = [System.Collections.Generic.List[PSObject]]::new()
                        if ($IncludeRecommendations) {
                            Write-Verbose "Retrieving recommendations for subscription: $($Subscription.displayName)"

                            $RecommendationsUri      = "$($Script:ArmBaseUri)/subscriptions/$($Subscription.subscriptionId)/providers/Microsoft.Security/assessments?api-version=2020-01-01"
                            $RecommendationsResponse = Invoke-RestMethod -Uri $RecommendationsUri -Headers $Script:ArmHeaders -Method GET -ErrorAction SilentlyContinue

                            foreach ($Assessment in ($RecommendationsResponse.value ?? @())) {
                                if ($Assessment.properties.status.code -eq 'Unhealthy') {
                                    $Recommendation = [PSCustomObject]@{
                                        SubscriptionId         = $Subscription.subscriptionId
                                        SubscriptionName       = $Subscription.displayName
                                        AssessmentId           = $Assessment.id
                                        DisplayName            = $Assessment.properties.displayName
                                        Description            = $Assessment.properties.description
                                        RemediationDescription = $Assessment.properties.remediationDescription
                                        Severity               = $Assessment.properties.status.severity
                                        Category               = $Assessment.properties.categories -join '; '
                                        ResourceType           = $Assessment.properties.resourceDetails.source
                                        LastStatusChangeDate   = $Assessment.properties.timeGenerated
                                    }
                                    $SubscriptionRecommendations.Add($Recommendation)
                                    $OverallRecommendations.Add($Recommendation)
                                }
                            }
                        }

                        $SubscriptionScore = [PSCustomObject]@{
                            SubscriptionId          = $Subscription.subscriptionId
                            SubscriptionName        = $Subscription.displayName
                            TenantId                = $Subscription.tenantId
                            State                   = $Subscription.state
                            CurrentScore            = [math]::Round($SecureScoreResponse.properties.score.current, 2)
                            MaxScore                = [math]::Round($SecureScoreResponse.properties.score.max, 2)
                            ScorePercentage         = if ($SecureScoreResponse.properties.score.max -gt 0) {
                                [math]::Round(($SecureScoreResponse.properties.score.current / $SecureScoreResponse.properties.score.max) * 100, 1)
                            } else { 0 }
                            Weight                  = $SecureScoreResponse.properties.weight ?? 0
                            LastCalculatedDate      = $SecureScoreResponse.properties.lastAssessedDate ?? $SecureScoreResponse.properties.createdDate
                            Controls                = $Controls.properties
                            TotalControls           = $Controls.Count
                            HealthyControls         = $HealthyControls
                            UnhealthyControls       = $UnhealthyControls
                            NotApplicableControls   = $NotApplicableControls
                            SecurityCenterEnabled   = $true
                            SecurityRecommendations = $SubscriptionRecommendations
                        }

                        $SubscriptionSecureScores.Add($SubscriptionScore)

                        # Get regulatory compliance if requested
                        if ($IncludeComplianceScore) {
                            Write-Verbose "Retrieving compliance data for subscription: $($Subscription.displayName)"

                            $ComplianceUri = "$($Script:ArmBaseUri)/subscriptions/$($Subscription.subscriptionId)/providers/Microsoft.Security/regulatoryComplianceStandards?api-version=2019-01-01-preview"
                            $ComplianceResponse = Invoke-RestMethod -Uri $ComplianceUri -Headers $Script:ArmHeaders -Method GET -ErrorAction SilentlyContinue

                            foreach ($Standard in ($ComplianceResponse.value ?? @())) {
                                $ComplianceData.Add([PSCustomObject]@{
                                        SubscriptionId      = $Subscription.subscriptionId
                                        SubscriptionName    = $Subscription.displayName
                                        StandardName        = $Standard.properties.displayName
                                        StandardId          = $Standard.name
                                        State               = $Standard.properties.state
                                        PassedControls      = $Standard.properties.passedControls
                                        FailedControls      = $Standard.properties.failedControls
                                        SkippedControls     = $Standard.properties.skippedControls
                                        UnsupportedControls = $Standard.properties.unsupportedControls
                                    })
                            }
                        }

                    } catch {
                        # Handle subscriptions without Security Center or insufficient permissions
                        Write-Verbose "Could not retrieve secure score for subscription $($Subscription.displayName): $($_.Exception.Message)"

                        $ProcessingErrors.Add([PSCustomObject]@{
                                SubscriptionId   = $Subscription.subscriptionId
                                SubscriptionName = $Subscription.displayName
                                Error            = $_.Exception.Message
                                ErrorType        = if ($_.Exception.Message -match '403|Forbidden') { 'Insufficient Permissions' }
                                elseif ($_.Exception.Message -match '404|Not Found') { 'Security Center Not Enabled' }
                                else { 'API Error' }
                            })

                        # Add placeholder entry for failed subscriptions
                        $SubscriptionSecureScores.Add([PSCustomObject]@{
                                SubscriptionId        = $Subscription.subscriptionId
                                SubscriptionName      = $Subscription.displayName
                                TenantId              = $Subscription.tenantId
                                State                 = $Subscription.state
                                CurrentScore          = 0
                                MaxScore              = 0
                                ScorePercentage       = 0
                                Weight                = 0
                                LastCalculatedDate    = $null
                                TotalControls         = 0
                                HealthyControls       = 0
                                UnhealthyControls     = 0
                                NotApplicableControls = 0
                                SecurityCenterEnabled = $false
                                Error                 = $_.Exception.Message
                            })
                    }
                } catch {
                    Write-Warning "Failed to process subscription $($Subscription.displayName): $($_.Exception.Message)"
                    continue
                }
            }

            # Calculate aggregated statistics
            $ValidScores = $SubscriptionSecureScores | Where-Object { $_.SecurityCenterEnabled -eq $true }

            if ($ValidScores.Count -eq 0) {
                Write-Warning 'No subscriptions with valid secure score data found. Check Security Center enablement and RBAC permissions.'
                return
            }

            # Calculate aggregated stats using single-pass accumulation
            $ScoreStats = @{
                TotalCurrentScore = 0
                TotalMaxScore     = 0
                HighestScore      = 0
                LowestScore       = [double]::MaxValue
                ScoreSum          = 0
                PercentageSum     = 0
            }
            foreach ($VS in $ValidScores) {
                $ScoreStats.TotalCurrentScore += $VS.CurrentScore
                $ScoreStats.TotalMaxScore     += $VS.MaxScore
                $ScoreStats.ScoreSum          += $VS.CurrentScore
                $ScoreStats.PercentageSum     += $VS.ScorePercentage
                if ($VS.CurrentScore -gt $ScoreStats.HighestScore) { $ScoreStats.HighestScore = $VS.CurrentScore }
                if ($VS.CurrentScore -lt $ScoreStats.LowestScore) { $ScoreStats.LowestScore = $VS.CurrentScore }
            }
            if ($ScoreStats.LowestScore -eq [double]::MaxValue) { $ScoreStats.LowestScore = 0 }

            $RecommendationStats = @{ Critical = 0; High = 0; Medium = 0 }
            foreach ($Rec in $OverallRecommendations) {
                switch ($Rec.Severity) {
                    'High' { $RecommendationStats.Critical++ }
                    'Medium' { $RecommendationStats.High++ }
                    'Low' { $RecommendationStats.Medium++ }
                }
            }

            $ValidScoreCount = $ValidScores.Count
            $AggregatedStats = [PSCustomObject]@{
                TotalSubscriptions              = $AllSubscriptions.Count
                SubscriptionsWithSecurityCenter = $ValidScoreCount
                SubscriptionsWithErrors         = $ProcessingErrors.Count
                AverageScore                    = if ($ValidScoreCount -gt 0) { [math]::Round($ScoreStats.ScoreSum / $ValidScoreCount, 2) } else { 0 }
                AveragePercentage               = if ($ValidScoreCount -gt 0) { [math]::Round($ScoreStats.PercentageSum / $ValidScoreCount, 1) } else { 0 }
                HighestScore                    = $ScoreStats.HighestScore
                LowestScore                     = $ScoreStats.LowestScore
                TotalCurrentScore               = $ScoreStats.TotalCurrentScore
                TotalMaxScore                   = $ScoreStats.TotalMaxScore
                OverallPercentage               = if ($ScoreStats.TotalMaxScore -gt 0) {
                    [math]::Round(($ScoreStats.TotalCurrentScore / $ScoreStats.TotalMaxScore) * 100, 1)
                } else { 0 }
                TotalRecommendations            = $OverallRecommendations.Count
                CriticalRecommendations         = $RecommendationStats.Critical
                HighRecommendations             = $RecommendationStats.High
                MediumRecommendations           = $RecommendationStats.Medium
            }

            # Get historical data if requested
            $HistoricalScores = [System.Collections.Generic.List[PSObject]]::new()
            $TrendAnalysis = $null
            if ($IncludeHistoricalData -and $ValidScores.Count -gt 0) {
                Write-Verbose "Retrieving historical secure score data (last $MaxHistoryDays days) for subscriptions..."

                foreach ($Subscription in ($AllSubscriptions | Where-Object { $_.subscriptionId -in ($ValidScores.SubscriptionId) })) {
                    try {
                        Write-Verbose "Retrieving historical data for subscription: $($Subscription.displayName)"

                        # Calculate date range for historical data
                        $StartDate = (Get-Date).AddDays(-$MaxHistoryDays).ToString('yyyy-MM-ddTHH:mm:ss.fffZ')

                        # Try to get historical secure score data
                        try {
                            $HistoryScoreUri = "$($Script:ArmBaseUri)/subscriptions/$($Subscription.subscriptionId)/providers/Microsoft.Security/secureScores?api-version=2020-01-01&`$filter=createdDateTime ge $StartDate&`$orderby=createdDateTime desc"
                            $HistoryResponse = Invoke-RestMethod -Uri $HistoryScoreUri -Headers $Script:ArmHeaders -Method GET -ErrorAction SilentlyContinue

                            if ($HistoryResponse.value) {
                                foreach ($HistoryEntry in $HistoryResponse.value) {
                                    $HistoricalScores.Add([PSCustomObject]@{
                                            SubscriptionId   = $Subscription.subscriptionId
                                            SubscriptionName = $Subscription.displayName
                                            Date             = $HistoryEntry.properties.createdDateTime
                                            CurrentScore     = [math]::Round($HistoryEntry.properties.score.current, 2)
                                            MaxScore         = [math]::Round($HistoryEntry.properties.score.max, 2)
                                            ScorePercentage  = if ($HistoryEntry.properties.score.max -gt 0) {
                                                [math]::Round(($HistoryEntry.properties.score.current / $HistoryEntry.properties.score.max) * 100, 1)
                                            } else { 0 }
                                        })
                                }
                            }
                        } catch {
                            Write-Verbose "Could not retrieve historical data for subscription $($Subscription.displayName): $($_.Exception.Message)"
                        }
                    } catch {
                        Write-Verbose "Failed to process historical data for subscription $($Subscription.displayName): $($_.Exception.Message)"
                        continue
                    }
                }

                # Calculate trend analysis if we have historical data
                if ($HistoricalScores.Count -gt 1) {
                    Write-Verbose "Calculating trend analysis from $($HistoricalScores.Count) historical data points"

                    # Group by subscription and calculate trends
                    $SubscriptionTrends = [System.Collections.Generic.List[PSObject]]::new()
                    $HistoricalScores | Group-Object SubscriptionId | ForEach-Object {
                        $SubHistory = $_.Group | Sort-Object Date
                        if ($SubHistory.Count -gt 1) {
                            $OldestScore = $SubHistory[0]
                            $LatestScore = $SubHistory[-1]
                            $ScoreChange = $LatestScore.CurrentScore - $OldestScore.CurrentScore
                            $PercentageChange = if ($OldestScore.CurrentScore -gt 0) {
                                [math]::Round((($LatestScore.CurrentScore - $OldestScore.CurrentScore) / $OldestScore.CurrentScore) * 100, 2)
                            } else { 0 }

                            $SubscriptionTrends.Add([PSCustomObject]@{
                                    SubscriptionId   = $_.Name
                                    SubscriptionName = $OldestScore.SubscriptionName
                                    PeriodDays       = $MaxHistoryDays
                                    ScoreChange      = [math]::Round($ScoreChange, 2)
                                    PercentageChange = $PercentageChange
                                    Trend            = if ($ScoreChange -gt 0) { 'Improving' } elseif ($ScoreChange -lt 0) { 'Declining' } else { 'Stable' }
                                    OldestScore      = $OldestScore.CurrentScore
                                    LatestScore      = $LatestScore.CurrentScore
                                    OldestDate       = $OldestScore.Date
                                    LatestDate       = $LatestScore.Date
                                    DataPoints       = $SubHistory.Count
                                })
                        }
                    }

                    # Calculate overall trend analysis using single-pass accumulation
                    if ($SubscriptionTrends.Count -gt 0) {
                        $TrendStats = @{
                            ScoreChangeSum       = 0
                            PercentageChangeSum  = 0
                            ImprovingCount       = 0
                            DecliningCount       = 0
                            StableCount          = 0
                        }
                        foreach ($SubTrend in $SubscriptionTrends) {
                            $TrendStats.ScoreChangeSum += $SubTrend.ScoreChange
                            $TrendStats.PercentageChangeSum += $SubTrend.PercentageChange
                            switch ($SubTrend.Trend) {
                                'Improving' { $TrendStats.ImprovingCount++ }
                                'Declining' { $TrendStats.DecliningCount++ }
                                'Stable' { $TrendStats.StableCount++ }
                            }
                        }
                        $TrendCount              = $SubscriptionTrends.Count
                        $OverallScoreChange      = $TrendStats.ScoreChangeSum
                        $AveragePercentageChange = if ($TrendCount -gt 0) { $TrendStats.PercentageChangeSum / $TrendCount } else { 0 }

                        $TrendAnalysis = [PSCustomObject]@{
                            PeriodDays                  = $MaxHistoryDays
                            TotalSubscriptionsWithTrend = $TrendCount
                            OverallScoreChange          = [math]::Round($OverallScoreChange, 2)
                            AveragePercentageChange     = [math]::Round($AveragePercentageChange, 2)
                            OverallTrend                = if ($OverallScoreChange -gt 0) { 'Improving' } elseif ($OverallScoreChange -lt 0) { 'Declining' } else { 'Stable' }
                            ImprovingSubscriptions      = $TrendStats.ImprovingCount
                            DecliningSubscriptions      = $TrendStats.DecliningCount
                            StableSubscriptions         = $TrendStats.StableCount
                            SubscriptionTrends          = $SubscriptionTrends
                            TotalHistoricalDataPoints   = $HistoricalScores.Count
                        }
                    }
                }

                Write-Verbose "Historical data processing completed. Found $($HistoricalScores.Count) historical entries"
            }

            # Build comprehensive report
            Write-Information "Azure Secure Score collection completed - $($ValidScores.Count) subscriptions processed" -InformationAction Continue

            [PSCustomObject][Ordered]@{
                Summary            = $AggregatedStats
                SubscriptionScores = $SubscriptionSecureScores | Sort-Object ScorePercentage -Descending
                ComplianceData     = if ($IncludeComplianceScore) {
                    $ComplianceData | Sort-Object SubscriptionName, StandardName
                } else {
                    @()
                }
                TrendAnalysis      = $TrendAnalysis
                HistoricalScores   = if ($IncludeHistoricalData) {
                    $HistoricalScores | Sort-Object Date -Descending
                } else {
                    @()
                }
                Recommendations    = $OverallRecommendations | Sort-Object Severity -Descending
                ProcessingErrors   = $ProcessingErrors
            }
        } catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Get-TntAzureSecureScoreReport failed: $($_.Exception.Message)", $_.Exception),
                'GetAzureSecureScoreReportError',
                [System.Management.Automation.ErrorCategory]::OperationStopped,
                $TenantId
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        } finally {
            # Cleanup connections
            if ($ConnectionInfo.ShouldDisconnect) {
                Disconnect-TntGraphSession -ConnectionState $ConnectionInfo
            }
        }
    }
}