root365.ps1

param(
    [string]$SPOAdminUrl
)

# ===============================
# ROOT365 audit runner (local SHELL scripts)
# ===============================
$ErrorActionPreference = "Stop"

$MaxRetries = 3
$ScriptDir = Join-Path $PSScriptRoot "SHELL"
$LogDir = Join-Path $PSScriptRoot "logs"
$RunStamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$ReportPath = Join-Path $PSScriptRoot "M365_Audit_Report_$RunStamp.json"
$CsvReportPath = Join-Path $PSScriptRoot "M365_Audit_Report_$RunStamp.csv"
$HtmlReportPath = Join-Path $PSScriptRoot "M365_Audit_Report_$RunStamp.html"
$ManifestCandidates = @(
    (Join-Path $PSScriptRoot "cis_v6_0_1_controls_manifest.json"),
    (Join-Path $PSScriptRoot "cis_v6_controls_manifest.json")
)
$ManifestPath = $null
$BenchmarkVersion = "Unknown"

function Get-PropertyValue {
    param(
        [Parameter(Mandatory = $true)]
        [object]$InputObject,
        [Parameter(Mandatory = $true)]
        [string]$PropertyName
    )

    if ($null -eq $InputObject -or $null -eq $InputObject.PSObject) {
        return $null
    }

    $Property = $InputObject.PSObject.Properties[$PropertyName]
    if ($null -ne $Property) {
        return $Property.Value
    }

    return $null
}

function Get-DerivedSeverity {
    param(
        [string]$Level
    )

    switch (($Level | ForEach-Object { $_.ToUpperInvariant() })) {
        "L2" { return "High" }
        "L1" { return "Medium" }
        default { return "Unknown" }
    }
}

function Convert-ToHtmlEncoded {
    param(
        [AllowNull()]
        [object]$Value
    )

    if ($null -eq $Value) {
        return ""
    }

    return [System.Net.WebUtility]::HtmlEncode([string]$Value)
}

function Convert-ToBoolean {
    param(
        [AllowNull()]
        [object]$Value
    )

    if ($null -eq $Value) {
        return $false
    }

    if ($Value -is [bool]) {
        return [bool]$Value
    }

    $Text = ([string]$Value).Trim()
    return ($Text -match '^(?i:true|1|yes|enabled)$')
}

function Convert-ToJsonSafe {
    param(
        [AllowNull()]
        [object]$InputObject,
        [int]$Depth = 8,
        [switch]$Compress
    )

    try {
        if ($Compress) {
            return ($InputObject | ConvertTo-Json -Depth $Depth -Compress -ErrorAction Stop)
        }

        return ($InputObject | ConvertTo-Json -Depth $Depth -ErrorAction Stop)
    }
    catch {
        $Fallback = [pscustomobject]@{
            SerializationError = $_.Exception.Message
            SerializationFallback = "Applied"
            InputType = if ($null -eq $InputObject) { "null" } else { $InputObject.GetType().FullName }
            InputCount = if ($InputObject -is [System.Collections.ICollection]) { $InputObject.Count } else { $null }
            Note = "JSON serialization failed for this payload. A compact fallback object was written instead."
        }

        if ($Compress) {
            return ($Fallback | ConvertTo-Json -Depth 4 -Compress)
        }

        return ($Fallback | ConvertTo-Json -Depth 4)
    }
}

if (-not (Test-Path $LogDir)) {
    New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
}

$AuditScripts = Get-ChildItem -Path $ScriptDir -File -Filter "*.ps1" | Sort-Object Name | Select-Object -ExpandProperty FullName
if (-not $AuditScripts) {
    throw "No PowerShell scripts found in $ScriptDir"
}

$ControlMetadataById = @{}
foreach ($CandidatePath in $ManifestCandidates) {
    if (Test-Path $CandidatePath) {
        $ManifestPath = $CandidatePath
        break
    }
}

if ($ManifestPath) {
    try {
        $Manifest = Get-Content -Path $ManifestPath -Raw | ConvertFrom-Json
        if ($Manifest.version) {
            $BenchmarkVersion = [string]$Manifest.version
        }

        foreach ($Entry in @($Manifest.entries)) {
            if ($Entry.id) {
                $ControlMetadataById[[string]$Entry.id] = $Entry
            }
        }
    }
    catch {
        Write-Host "Control manifest load warning: $($_.Exception.Message)" -ForegroundColor DarkYellow
    }
}
else {
    Write-Host "Control manifest file not found. Metadata enrichment will be limited." -ForegroundColor DarkYellow
}

$Results = @()

try {
    Write-Host "`nConnecting to Microsoft 365 services..." -ForegroundColor Cyan

    Connect-MgGraph -Scopes @(
"AccessReview.Read.All",
"AdministrativeUnit.Read.All",
"AdministrativeUnit.ReadWrite.All",
"Application.Read.All",
"Application.ReadWrite.All",
"AuditLog.Read.All",
"Directory.Read.All",
"Group.Read.All",
"IdentityRiskyUser.Read.All",
"NetworkAccessPolicy.Read.All",
"Policy.Read.All",
"Policy.Read.AuthenticationMethod",
"Policy.Read.ConditionalAccess",
"Policy.Read.DeviceConfiguration",
"Policy.ReadWrite.AuthenticationMethod",
"Policy.ReadWrite.ConditionalAccess",
"Policy.ReadWrite.DeviceConfiguration",
"Policy.ReadWrite.IdentityProtection",
"Policy.ReadWrite.PermissionGrant",
"Policy.ReadWrite.SecurityDefaults",
"Reports.Read.All",
"RoleManagement.Read.All",
"RoleManagement.Read.Directory",
"RoleManagementPolicy.Read.Directory",
"SecurityActions.Read.All",
"SecurityAlert.Read.All",
"SecurityIncident.Read.All",
"SecurityIncident.ReadWrite.All",
"SecurityEvents.Read.All",
"TeamworkDevice.Read.All",
"User.Read.All",
"UserAuthenticationMethod.Read.All",
"Sites.Read.All",
"Sites.FullControl.All",
"Sites.ReadWrite.All",
"Sites.Manage.All",
"Sites.Selected",

"DelegatedPermissionGrant.Read.All",
"DeviceManagementServiceConfig.Read.All",
"DeviceManagementConfiguration.Read.All",
"OnPremDirectorySynchronization.Read.All",
"OrgSettings-AppsAndServices.Read.All",
"OrgSettings-Forms.Read.All",
"Organization.Read.All",
"Domain.Read.All"
    )

    Connect-ExchangeOnline -ShowBanner:$false

    if (Get-Command -Name Connect-IPPSSession -ErrorAction SilentlyContinue) {
        Connect-IPPSSession | Out-Null
    }

    if (Get-Command -Name Connect-MicrosoftTeams -ErrorAction SilentlyContinue) {
        Connect-MicrosoftTeams
    }

    if (Get-Command -Name Connect-SPOService -ErrorAction SilentlyContinue) {
        if (-not $SPOAdminUrl) {
            try {
                $InitialDomain = Get-MgDomain -All |
                    Where-Object { $_.IsInitial -eq $true } |
                    Select-Object -First 1 -ExpandProperty Id

                if ($InitialDomain -match "^(?<tenant>[^.]+)\.onmicrosoft\.com$") {
                    $SPOAdminUrl = "https://$($Matches.tenant)-admin.sharepoint.com"
                }
            }
            catch {
                Write-Host "Unable to auto-detect SharePoint admin URL. Set -SPOAdminUrl to connect SPO in root365.ps1." -ForegroundColor DarkYellow
            }
        }

        if ($SPOAdminUrl) {
            Connect-SPOService -Url $SPOAdminUrl
        }
        else {
            Write-Host "Skipping SharePoint Online connection in root365.ps1 (SPOAdminUrl not set)." -ForegroundColor DarkYellow
        }
    }

    if (Get-Command -Name Connect-AzAccount -ErrorAction SilentlyContinue) {
        try {
            $AzContextAvailable = $null
            if (Get-Command -Name Get-AzContext -ErrorAction SilentlyContinue) {
                $AzContextAvailable = Get-AzContext -ErrorAction SilentlyContinue
            }

            if (-not $AzContextAvailable) {
                Connect-AzAccount -ErrorAction Stop | Out-Null
            }
        }
        catch {
            Write-Host "Azure connection warning for Fabric controls: $($_.Exception.Message)" -ForegroundColor DarkYellow
        }
    }

    if (Get-Command -Name Connect-PowerBIServiceAccount -ErrorAction SilentlyContinue) {
        try {
            Connect-PowerBIServiceAccount -ErrorAction Stop | Out-Null
        }
        catch {
            Write-Host "Power BI connection warning for Fabric controls: $($_.Exception.Message)" -ForegroundColor DarkYellow
        }
    }

    Write-Host "All services connected successfully." -ForegroundColor Green

    foreach ($Script in $AuditScripts) {
        $RetryCount = 0
        $Success = $false
        Start-Sleep 1
        while (-not $Success -and $RetryCount -lt $MaxRetries) {
            $RetryCount++

            Write-Host "`nRunning script [$Script] Attempt #$RetryCount" -ForegroundColor Yellow

            $LogFile = Join-Path $LogDir "$(Split-Path $Script -Leaf)-$(Get-Date -Format 'yyyyMMdd_HHmmss').log"

            $Result = [pscustomobject]@{
                Script          = $Script
                Attempt         = $RetryCount
                StartTime       = Get-Date
                EndTime         = $null
                Output          = $null
                Errors          = $null
                Status          = "UNKNOWN"
                Reason          = $null
                ExecutionStatus = "NotStarted"
                LogFile         = $LogFile
                CheckId         = $null
                Title           = $null
                Level           = $null
                Severity        = $null
                SeverityBasis   = $null
                ControlType     = $null
                Benchmark       = "CIS Microsoft 365 Foundations Benchmark"
                BenchmarkVersion = $BenchmarkVersion
                SourceManifest  = $ManifestPath
                ReferencePages  = $null
                ControlStatus   = $null
                Pass            = $null
                DataAudited     = $null
            }

            try {
                $Output = & $Script 2>&1
                $Result.Output = @($Output)
                $Result.ExecutionStatus = "Success"

                $ControlOutput = $Result.Output |
                    Where-Object {
                        $_ -and $_.PSObject -and $_.PSObject.Properties -and ($_.PSObject.Properties.Name -contains "CheckId")
                    } |
                    Select-Object -First 1

                if ($ControlOutput) {
                    try {
                        $Result.CheckId = Get-PropertyValue -InputObject $ControlOutput -PropertyName "CheckId"
                        $Result.Title = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Title"
                        $Result.Level = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Level"
                        $Result.ControlType = Get-PropertyValue -InputObject $ControlOutput -PropertyName "BenchmarkType"
                        if ([string]::IsNullOrWhiteSpace([string]$Result.ControlType)) {
                            $Result.ControlType = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Type"
                        }
                        $Result.ControlStatus = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Status"
                        $Result.Pass = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Pass"
                        $Result.DataAudited = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Evidence"
                        $ControlError = Get-PropertyValue -InputObject $ControlOutput -PropertyName "Error"

                        $RawStatus = $null
                        if ($null -ne $Result.ControlStatus) {
                            $RawStatus = $Result.ControlStatus.ToString().Trim().ToUpperInvariant()
                        }

                        if ($Result.Pass -eq $false) {
                            $Result.Status = "FAIL"
                            $Result.Reason = if ($ControlError) { [string]$ControlError } else { "Control result is non-compliant." }
                        }
                        elseif ($Result.Pass -eq $true) {
                            $Result.Status = "PASS"
                            $Result.Reason = $null
                        }
                        elseif ($RawStatus -eq "ERROR") {
                            $Result.Status = "ERROR"
                            $Result.Reason = if ($ControlError) { [string]$ControlError } else { "Control script returned ERROR status."
                            }
                            $Result.Errors = $Result.Reason
                        }
                        elseif ($RawStatus -eq "FAIL") {
                            $Result.Status = "FAIL"
                            $Result.Reason = if ($ControlError) { [string]$ControlError } else { "Control result is non-compliant." }
                        }
                        elseif (-not [string]::IsNullOrWhiteSpace($RawStatus)) {
                            $Result.Status = $RawStatus
                            if ($ControlError) {
                                $Result.Reason = [string]$ControlError
                            }
                        }
                        else {
                            $Result.Status = "INFO"
                            if ($ControlError) {
                                $Result.Reason = [string]$ControlError
                            }
                        }

                        if ([string]::IsNullOrWhiteSpace($Result.CheckId)) {
                            $Result.CheckId = [System.IO.Path]::GetFileNameWithoutExtension($Script)
                        }

                        $ControlMetadata = $null
                        if ($Result.CheckId -and $ControlMetadataById.ContainsKey([string]$Result.CheckId)) {
                            $ControlMetadata = $ControlMetadataById[[string]$Result.CheckId]
                        }

                        if ([string]::IsNullOrWhiteSpace([string]$Result.Title) -and $ControlMetadata -and $ControlMetadata.title) {
                            $Result.Title = [string]$ControlMetadata.title
                        }

                        if ([string]::IsNullOrWhiteSpace([string]$Result.Level) -and $ControlMetadata -and $ControlMetadata.level) {
                            $Result.Level = [string]$ControlMetadata.level
                        }

                        if ([string]::IsNullOrWhiteSpace([string]$Result.ControlType) -and $ControlMetadata -and $ControlMetadata.type) {
                            $Result.ControlType = [string]$ControlMetadata.type
                        }

                        if ($ControlMetadata -and $ControlMetadata.start_page) {
                            if ($ControlMetadata.end_page -and ([int]$ControlMetadata.end_page -ne [int]$ControlMetadata.start_page)) {
                                $Result.ReferencePages = "pp. $($ControlMetadata.start_page)-$($ControlMetadata.end_page)"
                            }
                            else {
                                $Result.ReferencePages = "p. $($ControlMetadata.start_page)"
                            }
                        }

                        if ([string]::IsNullOrWhiteSpace([string]$Result.Severity)) {
                            if ($ControlMetadata -and $ControlMetadata.severity) {
                                $Result.Severity = [string]$ControlMetadata.severity
                                $Result.SeverityBasis = "CIS v$BenchmarkVersion metadata"
                            }
                            else {
                                $Result.Severity = Get-DerivedSeverity -Level $Result.Level
                                $Result.SeverityBasis = "Derived from CIS level"
                            }
                        }

                        if ($Result.Status -eq "INFO") {
                            $Result.Status = "MANUAL_REVIEW"
                            if ([string]::IsNullOrWhiteSpace($Result.Reason)) {
                                $Result.Reason = "Informational evidence collected. Explicit PASS/FAIL threshold is not yet encoded for this control."
                            }
                        }

                        if ($Result.Status -eq "MANUAL_REVIEW" -and [string]::IsNullOrWhiteSpace($Result.Reason)) {
                            $Result.Reason = "Control requires manual validation or additional logic to determine compliance."
                        }
                    }
                    catch {
                        $WarningMessage = "Output mapping warning: $($_.Exception.Message)"
                        $Result.Status = "ERROR"
                        $Result.Reason = "Failed to parse control result output: $($_.Exception.Message)"
                        if ([string]::IsNullOrWhiteSpace($Result.CheckId)) {
                            $Result.CheckId = [System.IO.Path]::GetFileNameWithoutExtension($Script)
                        }
                        if ([string]::IsNullOrWhiteSpace($Result.Errors)) {
                            $Result.Errors = $WarningMessage
                        }
                        else {
                            $Result.Errors = "$($Result.Errors) | $WarningMessage"
                        }
                        Write-Host $WarningMessage -ForegroundColor DarkYellow
                    }
                }
                else {
                    $Result.CheckId = [System.IO.Path]::GetFileNameWithoutExtension($Script)
                    $Result.Status = "ERROR"
                    $Result.Reason = "Script executed but returned no control result (missing object with CheckId)."
                    $Result.Errors = $Result.Reason
                }

                if ($Result.CheckId -and $ControlMetadataById.ContainsKey([string]$Result.CheckId)) {
                    $ControlMetadata = $ControlMetadataById[[string]$Result.CheckId]

                    if ([string]::IsNullOrWhiteSpace([string]$Result.Title) -and $ControlMetadata.title) {
                        $Result.Title = [string]$ControlMetadata.title
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.Level) -and $ControlMetadata.level) {
                        $Result.Level = [string]$ControlMetadata.level
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.ControlType) -and $ControlMetadata.type) {
                        $Result.ControlType = [string]$ControlMetadata.type
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.ReferencePages) -and $ControlMetadata.start_page) {
                        if ($ControlMetadata.end_page -and ([int]$ControlMetadata.end_page -ne [int]$ControlMetadata.start_page)) {
                            $Result.ReferencePages = "pp. $($ControlMetadata.start_page)-$($ControlMetadata.end_page)"
                        }
                        else {
                            $Result.ReferencePages = "p. $($ControlMetadata.start_page)"
                        }
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.Severity) -and $ControlMetadata.severity) {
                        $Result.Severity = [string]$ControlMetadata.severity
                        $Result.SeverityBasis = "CIS v$BenchmarkVersion metadata"
                    }
                }

                if ([string]::IsNullOrWhiteSpace([string]$Result.Severity)) {
                    $Result.Severity = Get-DerivedSeverity -Level $Result.Level
                    if ([string]::IsNullOrWhiteSpace([string]$Result.SeverityBasis)) {
                        $Result.SeverityBasis = "Derived from CIS level"
                    }
                }

                if ($Result.Status -eq "MANUAL_REVIEW" -and [string]::IsNullOrWhiteSpace($Result.Reason)) {
                    $Result.Reason = "Control requires manual validation or additional logic to determine compliance."
                }
                $Success = $true
            }
            catch {
                $Result.Errors = $_.Exception.Message
                $Result.Status = "ERROR"
                $Result.ExecutionStatus = "Failed"
                $Result.Reason = "Script execution failed: $($_.Exception.Message)"
                if ([string]::IsNullOrWhiteSpace($Result.CheckId)) {
                    $Result.CheckId = [System.IO.Path]::GetFileNameWithoutExtension($Script)
                }

                if ($Result.CheckId -and $ControlMetadataById.ContainsKey([string]$Result.CheckId)) {
                    $ControlMetadata = $ControlMetadataById[[string]$Result.CheckId]

                    if ([string]::IsNullOrWhiteSpace([string]$Result.Title) -and $ControlMetadata.title) {
                        $Result.Title = [string]$ControlMetadata.title
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.Level) -and $ControlMetadata.level) {
                        $Result.Level = [string]$ControlMetadata.level
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.ControlType) -and $ControlMetadata.type) {
                        $Result.ControlType = [string]$ControlMetadata.type
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.ReferencePages) -and $ControlMetadata.start_page) {
                        if ($ControlMetadata.end_page -and ([int]$ControlMetadata.end_page -ne [int]$ControlMetadata.start_page)) {
                            $Result.ReferencePages = "pp. $($ControlMetadata.start_page)-$($ControlMetadata.end_page)"
                        }
                        else {
                            $Result.ReferencePages = "p. $($ControlMetadata.start_page)"
                        }
                    }

                    if ([string]::IsNullOrWhiteSpace([string]$Result.Severity) -and $ControlMetadata.severity) {
                        $Result.Severity = [string]$ControlMetadata.severity
                        $Result.SeverityBasis = "CIS v$BenchmarkVersion metadata"
                    }
                }

                if ([string]::IsNullOrWhiteSpace([string]$Result.Severity)) {
                    $Result.Severity = Get-DerivedSeverity -Level $Result.Level
                    if ([string]::IsNullOrWhiteSpace([string]$Result.SeverityBasis)) {
                        $Result.SeverityBasis = "Derived from CIS level"
                    }
                }
                Write-Host "Script failed: $($_.Exception.Message)" -ForegroundColor Red

                if ($RetryCount -lt $MaxRetries) {
                    Write-Host "Retrying in 5 seconds..." -ForegroundColor Cyan
                    Start-Sleep -Seconds 5
                }
            }

            $Result.EndTime = Get-Date
            $ResultJson = Convert-ToJsonSafe -InputObject $Result -Depth 8
            $ResultJson | Out-File -FilePath $LogFile -Encoding UTF8
            $Results += $Result
        }
    }
}
finally {
    if (Get-Command -Name Disconnect-SPOService -ErrorAction SilentlyContinue) {
        try {
            Disconnect-SPOService -ErrorAction Stop
        }
        catch {
        }
    }

    if (Get-Command -Name Disconnect-ExchangeOnline -ErrorAction SilentlyContinue) {
        try {
            Disconnect-ExchangeOnline -Confirm:$false -ErrorAction Stop
        }
        catch {
        }
    }

    if (Get-Command -Name Disconnect-MgGraph -ErrorAction SilentlyContinue) {
        try {
            Disconnect-MgGraph -ErrorAction Stop
        }
        catch {
        }
    }

    if (Get-Command -Name Disconnect-MicrosoftTeams -ErrorAction SilentlyContinue) {
        try {
            Disconnect-MicrosoftTeams -ErrorAction Stop
        }
        catch {
        }
    }

    if (Get-Command -Name Disconnect-PowerBIServiceAccount -ErrorAction SilentlyContinue) {
        try {
            Disconnect-PowerBIServiceAccount | Out-Null
        }
        catch {
        }
    }

    if (Get-Command -Name Disconnect-AzAccount -ErrorAction SilentlyContinue) {
        try {
            Disconnect-AzAccount -Scope Process -ErrorAction SilentlyContinue | Out-Null
        }
        catch {
        }
    }
}

$MasterReportJson = Convert-ToJsonSafe -InputObject $Results -Depth 8
$MasterReportJson | Out-File -FilePath $ReportPath -Encoding UTF8

$FinalResultsAll = $Results |
    Group-Object -Property Script |
    ForEach-Object {
        $_.Group | Sort-Object -Property Attempt -Descending | Select-Object -First 1
    } |
    Sort-Object -Property CheckId, Script

$SupplementalCheckIds = @("MFA.STATUS", "APP.ROLES.STATUS")
$MfaSupplementalResult = $FinalResultsAll | Where-Object { [string]$_.CheckId -eq "MFA.STATUS" } | Select-Object -First 1
$AppRolesSupplementalResult = $FinalResultsAll | Where-Object { [string]$_.CheckId -eq "APP.ROLES.STATUS" } | Select-Object -First 1
$FinalResults = @($FinalResultsAll | Where-Object { $SupplementalCheckIds -notcontains [string]$_.CheckId })

$MfaUserRows = @()
$MfaEnrichedRows = @()
$MfaUserSourceCsvPath = $null
$MfaUserReportPath = $null
$MfaRiskCounts = @{ Critical = 0; High = 0; Medium = 0; Low = 0 }
$MfaRoleCounts = @{ Admin = 0; External = 0; Member = 0 }
$MfaAuditCounts = @{ FAIL = 0; MANUAL_REVIEW = 0; PASS = 0 }
$MfaFinalScore = $null
$MfaScoreFormula = "Score = 100 - average(min(100, RiskPenalty x RoleMultiplier)); RiskPenalty: Critical=100, High=70, Medium=35, Low=0; RoleMultiplier: Admin=1.3, External=1.1, Member=1.0."
$MfaSectionMessage = ""

$AppRolesRows = @()
$AppRolesEnrichedRows = @()
$AppRolesSourceCsvPath = $null
$AppRolesCriticalCsvPath = $null
$AppRolesSectionMessage = ""
$AppRolesRiskCounts = @{ Critical = 0; Low = 0 }
$AppRolesTypeCounts = @{ Application = 0; Delegated = 0 }
$AppRolesSourceCounts = @{ EnterpriseApplicationConsent = 0; AppRegistrationRequested = 0 }
$AppRolesAuditCounts = @{ FAIL = 0; PASS = 0 }
$AppRolesFinalScore = $null
$AppRolesScoreFormula = "Score = 100 - ((CriticalPermissions / TotalPermissions) x 100)."

if ($MfaSupplementalResult -and $MfaSupplementalResult.DataAudited) {
    $MfaUserSourceCsvPath = [string](Get-PropertyValue -InputObject $MfaSupplementalResult.DataAudited -PropertyName "CsvExportPath")
    if (-not [string]::IsNullOrWhiteSpace($MfaUserSourceCsvPath) -and (Test-Path $MfaUserSourceCsvPath)) {
        try {
            $MfaUserRows = @(Import-Csv -Path $MfaUserSourceCsvPath)
        }
        catch {
            $MfaSectionMessage = "Failed to import MFA user source CSV: $($_.Exception.Message)"
        }
    }
    else {
        $MfaSectionMessage = "MFA user CSV path not available."
    }
}
else {
    $MfaSectionMessage = "MFA supplemental data is not available in this run."
}

if (@($MfaUserRows).Count -gt 0) {
    $PenaltyByRisk = @{
        Critical = 100
        High = 70
        Medium = 35
        Low = 0
    }
    $RoleMultiplier = @{
        Admin = 1.3
        External = 1.1
        Member = 1.0
    }

    $TotalPenalty = 0.0
    foreach ($Row in $MfaUserRows) {
        $IsAdmin = Convert-ToBoolean -Value $Row.IsAdmin
        $IsExternal = Convert-ToBoolean -Value $Row.IsExternal
        $RoleClass = if ($IsAdmin) { "Admin" } elseif ($IsExternal) { "External" } else { "Member" }

        $Risk = [string]$Row.Risk
        if ($MfaRiskCounts.ContainsKey($Risk) -eq $false) {
            $Risk = "Low"
        }

        $UserAuditResult = switch ($Risk) {
            "Critical" { "FAIL" }
            "High" { "FAIL" }
            "Medium" { "MANUAL_REVIEW" }
            default { "PASS" }
        }

        $RiskPenalty = [double]$PenaltyByRisk[$Risk]
        $Multiplier = [double]$RoleMultiplier[$RoleClass]
        $RowPenalty = [math]::Min(100.0, ($RiskPenalty * $Multiplier))
        $TotalPenalty += $RowPenalty

        $MfaRiskCounts[$Risk]++
        $MfaRoleCounts[$RoleClass]++
        $MfaAuditCounts[$UserAuditResult]++

        $MfaEnrichedRows += [pscustomobject]@{
            Name = [string]$Row.Name
            Id = [string]$Row.Id
            UserPrincipalName = [string]$Row.UserPrincipalName
            RoleClass = $RoleClass
            Risk = $Risk
            UserAuditResult = $UserAuditResult
            IsEnabled = [string]$Row.IsEnabled
            IsExternal = [string]$Row.IsExternal
            IsAdmin = [string]$Row.IsAdmin
            IsSSPRCapable = [string]$Row.IsSSPRCapable
            IsSSPREnabled = [string]$Row.IsSSPREnabled
            IsSSPRRegistered = [string]$Row.IsSSPRRegistered
            IsMFACapable = [string]$Row.IsMFACapable
            IsMFARegistered = [string]$Row.IsMFARegistered
            RegisteredMFAMethods = [string]$Row.RegisteredMFAMethods
            DefaultMFAMethod = [string]$Row.DefaultMFAMethod
            SystemPreferredMethodEnforced = [string]$Row.SystemPreferredMethodEnforced
            SystemEnforcedMethod = [string]$Row.SystemEnforcedMethod
            UserPreferredMFAMethod = [string]$Row.UserPreferredMFAMethod
            IsPasswordlessCapable = [string]$Row.IsPasswordlessCapable
            MemberOf = [string]$Row.MemberOf
            AppliedCAPolicies = [string]$Row.AppliedCAPolicies
            CAPoliciesNotApplied = [string]$Row.CAPoliciesNotApplied
            PossibleCAGaps = [string]$Row.PossibleCAGaps
            ScorePenalty = [math]::Round($RowPenalty, 2)
        }
    }

    if (@($MfaEnrichedRows).Count -gt 0) {
        $AveragePenalty = $TotalPenalty / @($MfaEnrichedRows).Count
        $MfaFinalScore = [math]::Round([math]::Max(0.0, (100.0 - $AveragePenalty)), 2)
        $MfaUserReportPath = Join-Path $PSScriptRoot "M365_MFA_User_Audit_$RunStamp.csv"
        $MfaEnrichedRows | Export-Csv -Path $MfaUserReportPath -NoTypeInformation -Encoding UTF8
    }
}

if ($AppRolesSupplementalResult -and $AppRolesSupplementalResult.DataAudited) {
    $AppRolesSourceCsvPath = [string](Get-PropertyValue -InputObject $AppRolesSupplementalResult.DataAudited -PropertyName "CsvExportPath")
    $AppRolesCriticalCsvPath = [string](Get-PropertyValue -InputObject $AppRolesSupplementalResult.DataAudited -PropertyName "CriticalCsvExportPath")

    if (-not [string]::IsNullOrWhiteSpace($AppRolesSourceCsvPath) -and (Test-Path $AppRolesSourceCsvPath)) {
        try {
            $AppRolesRows = @(Import-Csv -Path $AppRolesSourceCsvPath)
        }
        catch {
            $AppRolesSectionMessage = "Failed to import app role source CSV: $($_.Exception.Message)"
        }
    }
    else {
        $AppRolesSectionMessage = "App role source CSV path not available."
    }
}
else {
    $AppRolesSectionMessage = "App role supplemental data is not available in this run."
}

if (@($AppRolesRows).Count -gt 0) {
    $CriticalCount = 0
    foreach ($Row in $AppRolesRows) {
        $PermissionType = [string]$Row.PermissionType
        if ($AppRolesTypeCounts.ContainsKey($PermissionType) -eq $false) {
            $PermissionType = "Delegated"
        }

        $AssignmentSource = [string]$Row.AssignmentSource
        if ($AppRolesSourceCounts.ContainsKey($AssignmentSource) -eq $false) {
            $AssignmentSource = "AppRegistrationRequested"
        }

        $Risk = [string]$Row.Risk
        if ($AppRolesRiskCounts.ContainsKey($Risk) -eq $false) {
            $Risk = "Low"
        }

        $IsCritical = Convert-ToBoolean -Value $Row.IsCritical
        if ($IsCritical -or $Risk -eq "Critical") {
            $Risk = "Critical"
            $IsCritical = $true
        }

        $AuditResult = if ($IsCritical -and $PermissionType -eq "Application") { "FAIL" } else { "PASS" }
        if ($AuditResult -eq "FAIL") {
            $CriticalCount++
        }

        $AppRolesRiskCounts[$Risk]++
        $AppRolesTypeCounts[$PermissionType]++
        $AppRolesSourceCounts[$AssignmentSource]++
        $AppRolesAuditCounts[$AuditResult]++

        $AppRolesEnrichedRows += [pscustomobject]@{
            AppName = [string]$Row.AppName
            AppObjectId = [string]$Row.AppObjectId
            AppClientId = [string]$Row.AppClientId
            AssignmentSource = [string]$Row.AssignmentSource
            PermissionType = [string]$Row.PermissionType
            ResourceAppName = [string]$Row.ResourceAppName
            ResourceAppId = [string]$Row.ResourceAppId
            PermissionId = [string]$Row.PermissionId
            PermissionName = [string]$Row.PermissionName
            Owners = [string]$Row.Owners
            CreatedDateTime = [string]$Row.CreatedDateTime
            Disabled = [string]$Row.Disabled
            Risk = $Risk
            IsCritical = $IsCritical
            UserAuditResult = $AuditResult
            Reason = [string]$Row.Reason
        }
    }

    if (@($AppRolesEnrichedRows).Count -gt 0) {
        $AppRolesFinalScore = [math]::Round([math]::Max(0.0, (100.0 - (($CriticalCount * 100.0) / @($AppRolesEnrichedRows).Count))), 2)
    }
}

$CsvRows = $FinalResults | ForEach-Object {
    [pscustomobject]@{
        CheckId         = $_.CheckId
        Title           = $_.Title
        Level           = $_.Level
        Severity        = $_.Severity
        SeverityBasis   = $_.SeverityBasis
        ControlType     = $_.ControlType
        Benchmark       = $_.Benchmark
        BenchmarkVersion = $_.BenchmarkVersion
        ReferencePages  = $_.ReferencePages
        Script          = $_.Script
        Attempt         = $_.Attempt
        Result          = $_.Status
        ExecutionStatus = $_.ExecutionStatus
        ControlStatus   = $_.ControlStatus
        Pass            = $_.Pass
        Reason          = $_.Reason
        DataAudited     = if ($null -ne $_.DataAudited) { Convert-ToJsonSafe -InputObject $_.DataAudited -Depth 12 -Compress } else { $null }
        Error           = $_.Errors
        StartTime       = $_.StartTime
        EndTime         = $_.EndTime
        DurationSeconds = if ($_.StartTime -and $_.EndTime) { [math]::Round((New-TimeSpan -Start $_.StartTime -End $_.EndTime).TotalSeconds, 2) } else { $null }
        LogFile         = $_.LogFile
    }
}

$CsvRows | Export-Csv -Path $CsvReportPath -NoTypeInformation -Encoding UTF8

$StatusOrder = @("PASS", "FAIL", "MANUAL_REVIEW", "ERROR", "INFO", "UNKNOWN")
$StatusColors = @{
    PASS = "#1B5E20"
    FAIL = "#B71C1C"
    MANUAL_REVIEW = "#E65100"
    ERROR = "#6A1B9A"
    INFO = "#01579B"
    UNKNOWN = "#455A64"
}

$StatusCounts = @{}
foreach ($ResultRow in $FinalResults) {
    $StatusKey = [string]$ResultRow.Status
    if ([string]::IsNullOrWhiteSpace($StatusKey)) {
        $StatusKey = "UNKNOWN"
    }
    if (-not $StatusCounts.ContainsKey($StatusKey)) {
        $StatusCounts[$StatusKey] = 0
    }
    $StatusCounts[$StatusKey]++
}

$StatusSummary = @()
foreach ($StatusKey in $StatusOrder) {
    if ($StatusCounts.ContainsKey($StatusKey)) {
        $StatusSummary += [pscustomobject]@{
            Status = $StatusKey
            Count  = [int]$StatusCounts[$StatusKey]
            Color  = if ($StatusColors.ContainsKey($StatusKey)) { $StatusColors[$StatusKey] } else { "#37474F" }
        }
    }
}

foreach ($StatusKey in ($StatusCounts.Keys | Sort-Object)) {
    if ($StatusOrder -notcontains $StatusKey) {
        $StatusSummary += [pscustomobject]@{
            Status = $StatusKey
            Count  = [int]$StatusCounts[$StatusKey]
            Color  = "#37474F"
        }
    }
}

$TotalControls = @($FinalResults).Count

$StackSegments = @()
if ($TotalControls -gt 0) {
    foreach ($Item in $StatusSummary) {
        if ($Item.Count -gt 0) {
            $Pct = [math]::Round(($Item.Count * 100.0) / $TotalControls, 2)
            $Title = Convert-ToHtmlEncoded "$($Item.Status): $($Item.Count)"
            $StackSegments += "<div class='stack-segment' style='width:$Pct%; background:$($Item.Color)' title='$Title'></div>"
        }
    }
}

$BarRows = foreach ($Item in $StatusSummary) {
    $Pct = if ($TotalControls -gt 0) { [math]::Round(($Item.Count * 100.0) / $TotalControls, 1) } else { 0 }
    $StatusLabel = Convert-ToHtmlEncoded $Item.Status
    "<div class='bar-row'>
        <div class='bar-label'>$StatusLabel</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($Item.Color)'></div></div>
        <div class='bar-value'>$($Item.Count) ($Pct`%)</div>
    </div>"

}

$ControlRows = foreach ($Control in $FinalResults) {
    $Status = if ([string]::IsNullOrWhiteSpace([string]$Control.Status)) { "UNKNOWN" } else { [string]$Control.Status }
    $StatusColor = if ($StatusColors.ContainsKey($Status)) { $StatusColors[$Status] } else { "#37474F" }
    $StatusBadge = "<span class='status-badge' style='background:$StatusColor'>$([System.Net.WebUtility]::HtmlEncode($Status))</span>"

    $ReasonText = if ([string]::IsNullOrWhiteSpace([string]$Control.Reason)) { "" } else { [string]$Control.Reason }
    $ErrorText = if ([string]::IsNullOrWhiteSpace([string]$Control.Errors)) { "" } else { [string]$Control.Errors }
    $DetailsText = if ($ReasonText -and $ErrorText) { "$ReasonText | $ErrorText" } elseif ($ReasonText) { $ReasonText } else { $ErrorText }
    $DataAuditedCompact = if ($null -ne $Control.DataAudited) { (Convert-ToJsonSafe -InputObject $Control.DataAudited -Depth 12 -Compress) } else { "" }
    if ($DataAuditedCompact.Length -gt 600) {
        $DataAuditedCompact = $DataAuditedCompact.Substring(0, 600) + "...(truncated)"
    }

    "<tr>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Control.CheckId))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Control.Title))</td>
        <td>$StatusBadge</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Control.Severity))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Control.Level))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Control.ControlType))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Control.ReferencePages))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode($DetailsText))</td>
        <td><details><summary>Data</summary><pre>$([System.Net.WebUtility]::HtmlEncode($DataAuditedCompact))</pre></details></td>
    </tr>"

}

$GeneratedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$StackHtml = $StackSegments -join "`n"
$BarsHtml = $BarRows -join "`n"
$ControlsHtml = $ControlRows -join "`n"
$ManifestDisplay = if ($ManifestPath) { "$ManifestPath (v$BenchmarkVersion)" } else { "Not loaded" }

$MfaRiskOrder = @("Critical", "High", "Medium", "Low")
$MfaAuditOrder = @("FAIL", "MANUAL_REVIEW", "PASS")
$MfaRoleOrder = @("Admin", "External", "Member")
$MfaRiskColors = @{
    Critical = "#B71C1C"
    High = "#E65100"
    Medium = "#F9A825"
    Low = "#1B5E20"
}
$MfaAuditColors = @{
    FAIL = "#B71C1C"
    MANUAL_REVIEW = "#E65100"
    PASS = "#1B5E20"
}
$MfaRoleColors = @{
    Admin = "#6A1B9A"
    External = "#01579B"
    Member = "#2E7D32"
}

$MfaRiskBars = @()
foreach ($Key in $MfaRiskOrder) {
    $Count = if ($MfaRiskCounts.ContainsKey($Key)) { [int]$MfaRiskCounts[$Key] } else { 0 }
    $Pct = if (@($MfaEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($MfaEnrichedRows).Count, 1) } else { 0 }
    $MfaRiskBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($MfaRiskColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$MfaAuditBars = @()
foreach ($Key in $MfaAuditOrder) {
    $Count = if ($MfaAuditCounts.ContainsKey($Key)) { [int]$MfaAuditCounts[$Key] } else { 0 }
    $Pct = if (@($MfaEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($MfaEnrichedRows).Count, 1) } else { 0 }
    $MfaAuditBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($MfaAuditColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$MfaRoleBars = @()
foreach ($Key in $MfaRoleOrder) {
    $Count = if ($MfaRoleCounts.ContainsKey($Key)) { [int]$MfaRoleCounts[$Key] } else { 0 }
    $Pct = if (@($MfaEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($MfaEnrichedRows).Count, 1) } else { 0 }
    $MfaRoleBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($MfaRoleColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$MfaUserRowsHtml = @()
foreach ($Row in $MfaEnrichedRows) {
    $MfaUserRowsHtml += "<tr>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.UserPrincipalName))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.Name))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.RoleClass))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.Risk))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.UserAuditResult))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.IsMFARegistered))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.IsMFACapable))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.DefaultMFAMethod))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.AppliedCAPolicies))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.PossibleCAGaps))</td>
    </tr>"

}

$MfaScoreDisplay = if ($null -ne $MfaFinalScore) { "$MfaFinalScore / 100" } else { "N/A" }
$MfaUserSourceDisplay = if (-not [string]::IsNullOrWhiteSpace($MfaUserSourceCsvPath)) { $MfaUserSourceCsvPath } else { "N/A" }
$MfaUserExportDisplay = if (-not [string]::IsNullOrWhiteSpace($MfaUserReportPath)) { $MfaUserReportPath } else { "N/A" }
$MfaSectionInfo = if ([string]::IsNullOrWhiteSpace($MfaSectionMessage)) { "MFA user analysis loaded." } else { $MfaSectionMessage }
$MfaRiskBarsHtml = $MfaRiskBars -join "`n"
$MfaAuditBarsHtml = $MfaAuditBars -join "`n"
$MfaRoleBarsHtml = $MfaRoleBars -join "`n"
$MfaUsersHtml = $MfaUserRowsHtml -join "`n"

$AppRolesRiskOrder = @("Critical", "Low")
$AppRolesTypeOrder = @("Application", "Delegated")
$AppRolesSourceOrder = @("EnterpriseApplicationConsent", "AppRegistrationRequested")
$AppRolesAuditOrder = @("FAIL", "PASS")
$AppRolesRiskColors = @{
    Critical = "#B71C1C"
    Low = "#1B5E20"
}
$AppRolesTypeColors = @{
    Application = "#6A1B9A"
    Delegated = "#01579B"
}
$AppRolesSourceColors = @{
    EnterpriseApplicationConsent = "#E65100"
    AppRegistrationRequested = "#2E7D32"
}
$AppRolesAuditColors = @{
    FAIL = "#B71C1C"
    PASS = "#1B5E20"
}

$AppRolesRiskBars = @()
foreach ($Key in $AppRolesRiskOrder) {
    $Count = if ($AppRolesRiskCounts.ContainsKey($Key)) { [int]$AppRolesRiskCounts[$Key] } else { 0 }
    $Pct = if (@($AppRolesEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($AppRolesEnrichedRows).Count, 1) } else { 0 }
    $AppRolesRiskBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($AppRolesRiskColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$AppRolesTypeBars = @()
foreach ($Key in $AppRolesTypeOrder) {
    $Count = if ($AppRolesTypeCounts.ContainsKey($Key)) { [int]$AppRolesTypeCounts[$Key] } else { 0 }
    $Pct = if (@($AppRolesEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($AppRolesEnrichedRows).Count, 1) } else { 0 }
    $AppRolesTypeBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($AppRolesTypeColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$AppRolesSourceBars = @()
foreach ($Key in $AppRolesSourceOrder) {
    $Count = if ($AppRolesSourceCounts.ContainsKey($Key)) { [int]$AppRolesSourceCounts[$Key] } else { 0 }
    $Pct = if (@($AppRolesEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($AppRolesEnrichedRows).Count, 1) } else { 0 }
    $AppRolesSourceBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($AppRolesSourceColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$AppRolesAuditBars = @()
foreach ($Key in $AppRolesAuditOrder) {
    $Count = if ($AppRolesAuditCounts.ContainsKey($Key)) { [int]$AppRolesAuditCounts[$Key] } else { 0 }
    $Pct = if (@($AppRolesEnrichedRows).Count -gt 0) { [math]::Round(($Count * 100.0) / @($AppRolesEnrichedRows).Count, 1) } else { 0 }
    $AppRolesAuditBars += "<div class='bar-row'>
        <div class='bar-label'>$([System.Net.WebUtility]::HtmlEncode($Key))</div>
        <div class='bar-track'><div class='bar-fill' style='width:$Pct%; background:$($AppRolesAuditColors[$Key])'></div></div>
        <div class='bar-value'>$Count ($Pct`%)</div>
    </div>"

}

$AppRolesRowsHtml = @()
foreach ($Row in $AppRolesEnrichedRows) {
    $AppRolesRowsHtml += "<tr>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.AppName))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.AppClientId))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.PermissionType))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.PermissionName))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.ResourceAppName))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.AssignmentSource))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.Risk))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.UserAuditResult))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.Owners))</td>
        <td>$([System.Net.WebUtility]::HtmlEncode([string]$Row.Reason))</td>
    </tr>"

}

$AppRolesScoreDisplay = if ($null -ne $AppRolesFinalScore) { "$AppRolesFinalScore / 100" } else { "N/A" }
$AppRolesSourceDisplay = if (-not [string]::IsNullOrWhiteSpace($AppRolesSourceCsvPath)) { $AppRolesSourceCsvPath } else { "N/A" }
$AppRolesCriticalDisplay = if (-not [string]::IsNullOrWhiteSpace($AppRolesCriticalCsvPath)) { $AppRolesCriticalCsvPath } else { "N/A" }
$AppRolesSectionInfo = if ([string]::IsNullOrWhiteSpace($AppRolesSectionMessage)) { "App role analysis loaded." } else { $AppRolesSectionMessage }
$AppRolesRiskBarsHtml = $AppRolesRiskBars -join "`n"
$AppRolesTypeBarsHtml = $AppRolesTypeBars -join "`n"
$AppRolesSourceBarsHtml = $AppRolesSourceBars -join "`n"
$AppRolesAuditBarsHtml = $AppRolesAuditBars -join "`n"
$AppRolesUsersHtml = $AppRolesRowsHtml -join "`n"

$HtmlBody = @"
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>M365 Audit Report - $RunStamp</title>
  <style>
    body { font-family: Segoe UI, Arial, sans-serif; margin: 24px; color: #1f2937; background: #f8fafc; }
    h1, h2 { margin: 0 0 12px 0; }
    .meta { margin-bottom: 16px; color: #475569; }
    .panel { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px; margin-bottom: 18px; }
    .stack { display: flex; height: 20px; border-radius: 999px; overflow: hidden; background: #e2e8f0; margin-bottom: 12px; }
    .stack-segment { height: 100%; }
    .bar-row { display: grid; grid-template-columns: 170px 1fr 130px; gap: 10px; align-items: center; margin: 6px 0; }
    .bar-label { font-weight: 600; }
    .bar-track { background: #e2e8f0; border-radius: 6px; height: 14px; overflow: hidden; }
    .bar-fill { height: 100%; border-radius: 6px; }
    .bar-value { text-align: right; color: #334155; font-variant-numeric: tabular-nums; }
    table { border-collapse: collapse; width: 100%; background: #fff; border-radius: 8px; overflow: hidden; }
    thead th { background: #e2e8f0; color: #0f172a; font-weight: 600; text-align: left; padding: 10px; border-bottom: 1px solid #cbd5e1; position: sticky; top: 0; }
    tbody td { border-bottom: 1px solid #e2e8f0; padding: 10px; vertical-align: top; }
    .status-badge { color: #fff; font-weight: 700; font-size: 12px; padding: 4px 8px; border-radius: 999px; display: inline-block; }
    pre { white-space: pre-wrap; word-break: break-word; margin: 8px 0 0 0; font-size: 12px; color: #334155; }
    .table-wrap { max-height: 68vh; overflow: auto; border: 1px solid #e2e8f0; border-radius: 8px; }
    .score-card { display: inline-block; padding: 10px 14px; border-radius: 10px; background: #0f172a; color: #f8fafc; font-weight: 700; margin: 8px 0 12px 0; }
  </style>
</head>
<body>
  <h1>Microsoft 365 Audit Report</h1>
  <div class="meta">
    <div><strong>Generated:</strong> $GeneratedAt</div>
    <div><strong>Total Controls:</strong> $TotalControls</div>
    <div><strong>Benchmark:</strong> CIS Microsoft 365 Foundations Benchmark v$BenchmarkVersion</div>
    <div><strong>Control Metadata Source:</strong> $([System.Net.WebUtility]::HtmlEncode($ManifestDisplay))</div>
  </div>
 
  <div class="panel">
    <h2>Summary Chart</h2>
    <div class="stack">
      $StackHtml
    </div>
    $BarsHtml
  </div>
 
  <div class="panel">
    <h2>MFA User Risk Analysis</h2>
    <div><strong>Source CSV:</strong> $([System.Net.WebUtility]::HtmlEncode($MfaUserSourceDisplay))</div>
    <div><strong>Exported Enriched CSV:</strong> $([System.Net.WebUtility]::HtmlEncode($MfaUserExportDisplay))</div>
    <div><strong>Users Analyzed:</strong> $(@($MfaEnrichedRows).Count)</div>
    <div><strong>Info:</strong> $([System.Net.WebUtility]::HtmlEncode($MfaSectionInfo))</div>
    <div class="score-card">Final MFA Score: $([System.Net.WebUtility]::HtmlEncode($MfaScoreDisplay))</div>
    <div><small>$([System.Net.WebUtility]::HtmlEncode($MfaScoreFormula))</small></div>
 
    <h3>Risk Distribution</h3>
    $MfaRiskBarsHtml
 
    <h3>Role Distribution</h3>
    $MfaRoleBarsHtml
 
    <h3>User Audit Result Distribution</h3>
    $MfaAuditBarsHtml
 
    <h3>User-Level Results</h3>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>UserPrincipalName</th>
            <th>Name</th>
            <th>Role</th>
            <th>Risk</th>
            <th>Audit Result</th>
            <th>MFA Registered</th>
            <th>MFA Capable</th>
            <th>Default MFA Method</th>
            <th>Applied CA Policies</th>
            <th>Possible CA Gaps</th>
          </tr>
        </thead>
        <tbody>
          $MfaUsersHtml
        </tbody>
      </table>
    </div>
  </div>
 
  <div class="panel">
    <h2>Application Roles Risk Analysis</h2>
    <div><strong>Source CSV:</strong> $([System.Net.WebUtility]::HtmlEncode($AppRolesSourceDisplay))</div>
    <div><strong>Critical CSV:</strong> $([System.Net.WebUtility]::HtmlEncode($AppRolesCriticalDisplay))</div>
    <div><strong>Permissions Analyzed:</strong> $(@($AppRolesEnrichedRows).Count)</div>
    <div><strong>Info:</strong> $([System.Net.WebUtility]::HtmlEncode($AppRolesSectionInfo))</div>
    <div class="score-card">Final App Permission Score: $([System.Net.WebUtility]::HtmlEncode($AppRolesScoreDisplay))</div>
    <div><small>$([System.Net.WebUtility]::HtmlEncode($AppRolesScoreFormula))</small></div>
 
    <h3>Risk Distribution</h3>
    $AppRolesRiskBarsHtml
 
    <h3>Permission Type Distribution</h3>
    $AppRolesTypeBarsHtml
 
    <h3>Assignment Source Distribution</h3>
    $AppRolesSourceBarsHtml
 
    <h3>Audit Result Distribution</h3>
    $AppRolesAuditBarsHtml
 
    <h3>Permission-Level Results</h3>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>App Name</th>
            <th>Client ID</th>
            <th>Permission Type</th>
            <th>Permission</th>
            <th>Resource</th>
            <th>Assignment Source</th>
            <th>Risk</th>
            <th>Audit Result</th>
            <th>Owners</th>
            <th>Reason</th>
          </tr>
        </thead>
        <tbody>
          $AppRolesUsersHtml
        </tbody>
      </table>
    </div>
  </div>
 
  <div class="panel">
    <h2>Control Results</h2>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>CheckId</th>
            <th>Title</th>
            <th>Status</th>
            <th>Severity</th>
            <th>Level</th>
            <th>Type</th>
            <th>Pages</th>
            <th>Details</th>
            <th>Data Audited</th>
          </tr>
        </thead>
        <tbody>
          $ControlsHtml
        </tbody>
      </table>
    </div>
  </div>
</body>
</html>
"@


$HtmlBody | Out-File -FilePath $HtmlReportPath -Encoding UTF8

Write-Host "`nAudit completed. Master report saved to:" -ForegroundColor Green
Write-Host $ReportPath
Write-Host "`nCSV report saved to:" -ForegroundColor Green
Write-Host $CsvReportPath
Write-Host "`nHTML report saved to:" -ForegroundColor Green
Write-Host $HtmlReportPath
if (-not [string]::IsNullOrWhiteSpace([string]$MfaUserReportPath)) {
    Write-Host "`nMFA user report saved to:" -ForegroundColor Green
    Write-Host $MfaUserReportPath
}
if (-not [string]::IsNullOrWhiteSpace([string]$AppRolesSourceCsvPath)) {
    Write-Host "`nApp roles source report saved to:" -ForegroundColor Green
    Write-Host $AppRolesSourceCsvPath
}
if (-not [string]::IsNullOrWhiteSpace([string]$AppRolesCriticalCsvPath)) {
    Write-Host "`nApp roles critical report saved to:" -ForegroundColor Green
    Write-Host $AppRolesCriticalCsvPath
}
if ($ManifestPath) {
    Write-Host "`nControl metadata source:" -ForegroundColor Green
    Write-Host "$ManifestPath (v$BenchmarkVersion)"
}
Write-Host "`nIndividual script logs saved to:" -ForegroundColor Green
Write-Host $LogDir