Security/Get-SecureScoreReport.ps1

<#
.SYNOPSIS
    Retrieves the latest Microsoft Secure Score and per-control breakdown.
.DESCRIPTION
    Queries Microsoft Graph for the most recent Secure Score snapshot and reports
    the overall score summary (current, max, percentage) along with the average
    comparative score. Optionally exports a detailed improvement actions breakdown
    to a separate CSV file for remediation planning and client reporting.
 
    Requires Microsoft.Graph.Security module and SecurityEvents.Read.All permission.
.PARAMETER OutputPath
    Optional path to export the score summary as CSV. If not specified, results are
    returned to the pipeline.
.PARAMETER ImprovementActionsPath
    Optional path to export the per-control improvement actions breakdown as CSV.
    If not specified, improvement actions are not exported separately.
.EXAMPLE
    PS> . .\Common\Connect-Service.ps1
    PS> Connect-Service -Service Graph -Scopes 'SecurityEvents.Read.All'
    PS> .\Security\Get-SecureScoreReport.ps1
 
    Displays the latest Secure Score summary to the console.
.EXAMPLE
    PS> .\Security\Get-SecureScoreReport.ps1 -OutputPath '.\secure-score.csv' -ImprovementActionsPath '.\improvement-actions.csv'
 
    Exports the score summary and improvement actions to separate CSV files.
.EXAMPLE
    PS> .\Security\Get-SecureScoreReport.ps1 -Verbose
 
    Displays the latest Secure Score with verbose processing details.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [string]$OutputPath,

    [Parameter()]
    [string]$ImprovementActionsPath
)

$ErrorActionPreference = 'Stop'

# Verify Graph connection
if (-not (Assert-GraphConnection)) { return }

# Ensure required Graph submodule is loaded (PS 7.x does not auto-import)
Import-Module -Name Microsoft.Graph.Security -ErrorAction Stop

# Retrieve the most recent Secure Score
Write-Verbose "Retrieving the latest Secure Score..."
try {
    $secureScores = Get-MgSecuritySecureScore -Top 1 -Sort 'createdDateTime desc'
}
catch {
    Write-Error "Failed to retrieve Secure Score. Ensure SecurityEvents.Read.All permission is granted: $_"
    return
}

if (-not $secureScores -or @($secureScores).Count -eq 0) {
    Write-Warning "No Secure Score data found for this tenant."
    return
}

$latestScore = @($secureScores)[0]

# Calculate the percentage
$currentScore = $latestScore.CurrentScore
$maxScore = $latestScore.MaxScore
$percentage = if ($maxScore -gt 0) {
    [math]::Round(($currentScore / $maxScore) * 100, 2)
}
else {
    0
}

# Extract the average comparative score from the AverageComparativeScores collection
$averageComparative = 0
if ($latestScore.AverageComparativeScores) {
    $averageEntry = $latestScore.AverageComparativeScores |
        Where-Object { $_.Basis -eq 'AllTenants' } |
        Select-Object -First 1
    if ($averageEntry) {
        $averageComparative = $averageEntry.AverageScore
    }
}

# Build the score summary object
$scoreSummary = [PSCustomObject]@{
    CurrentScore           = $currentScore
    MaxScore               = $maxScore
    Percentage             = $percentage
    CreatedDateTime        = $latestScore.CreatedDateTime
    AverageComparativeScore = $averageComparative
}

Write-Verbose "Secure Score: $currentScore / $maxScore ($percentage%) as of $($latestScore.CreatedDateTime)"

# Process improvement actions from ControlScores
$improvementActions = [System.Collections.Generic.List[PSCustomObject]]::new()

if ($latestScore.ControlScores -and $latestScore.ControlScores.Count -gt 0) {
    Write-Verbose "Processing $($latestScore.ControlScores.Count) control scores..."

    foreach ($control in $latestScore.ControlScores) {
        # Extract additional properties from the AdditionalProperties dictionary
        $additionalProps = $control.AdditionalProperties

        $category = if ($additionalProps -and $additionalProps.ContainsKey('controlCategory')) {
            $additionalProps['controlCategory']
        }
        else {
            'N/A'
        }

        $implementationStatus = if ($additionalProps -and $additionalProps.ContainsKey('implementationStatus')) {
            $additionalProps['implementationStatus']
        }
        else {
            'N/A'
        }

        $userImpact = if ($additionalProps -and $additionalProps.ContainsKey('userImpact')) {
            $additionalProps['userImpact']
        }
        else {
            'N/A'
        }

        $threats = if ($additionalProps -and $additionalProps.ContainsKey('threats')) {
            $threatList = $additionalProps['threats']
            if ($threatList -is [System.Collections.IEnumerable] -and $threatList -isnot [string]) {
                ($threatList | ForEach-Object { $_.ToString() }) -join '; '
            }
            else {
                [string]$threatList
            }
        }
        else {
            'N/A'
        }

        $scoreImpact = if ($control.ScoreInPercentage) {
            $control.ScoreInPercentage
        }
        elseif ($additionalProps -and $additionalProps.ContainsKey('scoreInPercentage')) {
            $additionalProps['scoreInPercentage']
        }
        else {
            0
        }

        $controlCurrentScore = if ($null -ne $control.Score) {
            $control.Score
        }
        else {
            0
        }

        $controlMaxScore = if ($additionalProps -and $additionalProps.ContainsKey('maxScore')) {
            $additionalProps['maxScore']
        }
        else {
            0
        }

        $improvementActions.Add([PSCustomObject]@{
            ActionName           = $control.ControlName
            Category             = $category
            ScoreImpact          = $scoreImpact
            CurrentScore         = $controlCurrentScore
            MaxScore             = $controlMaxScore
            ImplementationStatus = $implementationStatus
            UserImpact           = $userImpact
            Threats              = $threats
        })
    }
}
else {
    Write-Verbose "No control score data found in the latest Secure Score."
}

# Export improvement actions if path is specified
if ($ImprovementActionsPath -and $improvementActions.Count -gt 0) {
    $improvementActions | Export-Csv -Path $ImprovementActionsPath -NoTypeInformation -Encoding UTF8
    Write-Verbose "Exported $($improvementActions.Count) improvement actions to $ImprovementActionsPath"
}

# Output the score summary
if ($OutputPath) {
    @($scoreSummary) | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported Secure Score summary to $OutputPath"
    if ($ImprovementActionsPath -and $improvementActions.Count -gt 0) {
        Write-Output "Exported $($improvementActions.Count) improvement actions to $ImprovementActionsPath"
    }
}
else {
    Write-Output $scoreSummary
    if ($improvementActions.Count -gt 0 -and -not $ImprovementActionsPath) {
        Write-Verbose "Use -ImprovementActionsPath to export the $($improvementActions.Count) improvement actions to CSV."
    }
}