Src/Private/Get-AbrEntraIDTenantOverview.ps1

function Get-AbrEntraIDTenantOverview {
    <#
    .SYNOPSIS
    Documents the Microsoft Entra ID tenant overview and licensing summary.
    .DESCRIPTION
        Collects and reports on:
          - Tenant identity (name, ID, domains)
          - SKU / license summary
          - Security defaults status
          - Password policy configuration
    .NOTES
        Version: 0.1.20
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Entra ID Tenant Overview for $TenantId." 
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Tenant Overview'
    }

    process {
        #region Tenant Overview
        # IMPORTANT: Section{} is always created first so any catch{} error paragraph
        # is written INSIDE the section -- never to the document root (which crashes PScribo).
        Section -Style Heading1 'Tenant Overview' {
            Paragraph "The following section provides a summary of the Microsoft Entra ID tenant configuration for $TenantId."
            BlankLine

            try {
                # Use direct REST to avoid MissingApiVersionParameter on Graph SDK v2.28
                $OrgResp = Invoke-MgGraphRequest -Method GET `
                    -Uri 'https://graph.microsoft.com/v1.0/organization?$select=id,displayName,verifiedDomains,countryLetterCode,preferredLanguage,createdDateTime,tenantType' `
                    -ErrorAction Stop
                $Org = if ($OrgResp.value) { $OrgResp.value[0] } else { $OrgResp }

                #region Tenant Details
                $TenantObj = [System.Collections.ArrayList]::new()
                $DefaultDomain = ($Org.verifiedDomains | Where-Object { $_.IsDefault }).Name
                $Domains       = ($Org.verifiedDomains | ForEach-Object { $_.Name }) -join ', '

                $tenantInObj = [ordered] @{
                    'Tenant Name'          = $Org.displayName
                    'Tenant ID'            = $Org.id
                    'Default Domain'       = $DefaultDomain
                    'Verified Domains'     = $Domains
                    'Country / Region'     = if ($Org.countryLetterCode) { $Org.countryLetterCode } else { '--' }
                    'Preferred Language'   = if ($Org.preferredLanguage)  { $Org.preferredLanguage  } else { '--' }
                    'Created Date'         = if ($Org.createdDateTime)    { ($Org.createdDateTime).ToString('yyyy-MM-dd') } else { '--' }
                    'Tenant Type'          = if ($Org.tenantType)         { $Org.tenantType         } else { 'AAD' }
                }
                $TenantObj.Add([pscustomobject](ConvertTo-HashToYN $tenantInObj)) | Out-Null

                $TableParams = @{ Name = "Tenant Details - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                    if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" }
                $TenantObj | Table @TableParams
                #endregion

                #region Security Defaults
                try {
                    $SecDefaults = Get-MgPolicyIdentitySecurityDefaultEnforcementPolicy -ErrorAction SilentlyContinue
                    if ($SecDefaults) {
                        $SecObj = [System.Collections.ArrayList]::new()
                        $secInObj = [ordered] @{
                            'Security Defaults Enabled' = $SecDefaults.IsEnabled
                        }
                        $SecObj.Add([pscustomobject](ConvertTo-HashToYN $secInObj)) | Out-Null
                    $null = (& {
                        if ($HealthCheck.EntraID.ConditionalAccess) {
                            $null = ($SecObj | Where-Object { $_.'Security Defaults Enabled' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                        }
                    })
                        $SecTableParams = @{ Name = "Security Defaults - $TenantId"; List = $true; ColumnWidths = 60, 40 }
                    if ($Report.ShowTableCaptions) { $SecTableParams['Caption'] = "- $($SecTableParams.Name)" }
                        $SecObj | Table @SecTableParams
                    }
                } catch {
                    Write-AbrSectionError -Section 'Security Defaults' -Message "$($_.Exception.Message)"
                }
                #endregion

                #region License / SKU Summary
                try {
                    $Skus = Get-MgSubscribedSku -ErrorAction SilentlyContinue
                    if ($Skus) {
                        $SkuObj = [System.Collections.ArrayList]::new()
                        foreach ($Sku in ($Skus | Sort-Object SkuPartNumber)) {
                            $skuInObj = [ordered] @{
                                'SKU / License'    = $Sku.SkuPartNumber
                                'Total Units'      = $Sku.PrepaidUnits.Enabled
                                'Assigned Units'   = $Sku.ConsumedUnits
                                'Available Units'  = ($Sku.PrepaidUnits.Enabled - $Sku.ConsumedUnits)
                                'Suspended Units'  = $Sku.PrepaidUnits.Suspended
                                'Warning Units'    = $Sku.PrepaidUnits.Warning
                            }
                            $SkuObj.Add([pscustomobject]$skuInObj) | Out-Null
                        }
                    $null = (& {if ($HealthCheck.EntraID.MFA) {
                    $null = ($SkuObj | Where-Object { $_.'Available Units' -le 0 } | Set-Style -Style Warning | Out-Null)
                    }})
                        $SkuTableParams = @{ Name = "License Summary - $TenantId"; List = $false; ColumnWidths = 30, 14, 14, 14, 14, 14 }
                    if ($Report.ShowTableCaptions) { $SkuTableParams['Caption'] = "- $($SkuTableParams.Name)" }
                        $SkuObj | Table @SkuTableParams
                        $null = ($script:ExcelSheets['License Summary'] = $SkuObj)
                    }
                } catch {
                    Write-AbrSectionError -Section 'License SKU retrieval' -Message "$($_.Exception.Message)"
                }
                #endregion

                #region Technical Notification Email
                try {
                    $NotifEmails = if ($MgOrg.TechnicalNotificationMails) { @($MgOrg.TechnicalNotificationMails) } else { @() }
                    $NotifObj = [System.Collections.ArrayList]::new()
                    if ($NotifEmails.Count -gt 0) {
                        foreach ($email in $NotifEmails) {
                            $null = $NotifObj.Add([pscustomobject][ordered]@{
                                'Notification Email' = $email
                                'Recommendation'     = if ($email -match 'group|dl\.|dist|shared|noreply') {
                                                           '[OK] Appears to be a shared mailbox or DL'
                                                       } else {
                                                           '[WARN] May be a personal mailbox -- consider using a distribution list'
                                                       }
                            })
                        }
                    } else {
                        $null = $NotifObj.Add([pscustomobject][ordered]@{
                            'Notification Email' = '[FAIL] Not configured'
                            'Recommendation'     = 'Set technical notification email in Entra admin center > Overview > Properties. Microsoft sends service alerts to this address.'
                        })
                    }
                    BlankLine
                    Paragraph 'Technical Notification Email Configuration:'
                    BlankLine
                    $null = (& { if ($HealthCheck.EntraID.TenantOverview) {
                        $null = ($NotifObj | Where-Object { $_.'Notification Email' -like '*FAIL*' } | Set-Style -Style Critical | Out-Null)
                        $null = ($NotifObj | Where-Object { $_.'Recommendation'    -like '*WARN*' } | Set-Style -Style Warning  | Out-Null)
                    }})
                    $NotifTableParams = @{ Name = "Technical Notification Emails - $TenantId"; List = $false; ColumnWidths = 40, 60 }
                    if ($Report.ShowTableCaptions) { $NotifTableParams['Caption'] = "- $($NotifTableParams.Name)" }
                    $NotifObj | Table @NotifTableParams
                } catch {
                    Write-AbrDebugLog "Notification email check failed: $($_.Exception.Message)" 'WARN' 'OVERVIEW'
                }
                #endregion

            } catch {
                Write-AbrSectionError -Section 'Tenant Overview' -Message "$($_.Exception.Message)"
            }
            #region Identity Secure Score
            try {
                Write-Host " - Retrieving Identity Secure Score..."
                $ScoreResp = Invoke-MgGraphRequest -Method GET `
                    -Uri 'https://graph.microsoft.com/v1.0/security/secureScores?$top=1' `
                    -ErrorAction SilentlyContinue
                $Score = if ($ScoreResp -and $ScoreResp.value) { $ScoreResp.value[0] } else { $null }
                if ($Score) {
                    $CurrentScore = [math]::Round($Score.currentScore, 1)
                    $MaxScore     = [math]::Round($Score.maxScore, 1)
                    $ScorePct     = if ($MaxScore -gt 0) { [math]::Round(($CurrentScore / $MaxScore) * 100, 0) } else { 0 }
                    $ScoreDate    = if ($Score.createdDateTime) { ($Score.createdDateTime).ToString('yyyy-MM-dd') } else { '--' }

                    Section -Style Heading2 'Identity Secure Score' {
                        Paragraph "Microsoft Identity Secure Score provides a quantifiable measure of security posture. A higher score indicates better alignment with Microsoft security best practices."
                        BlankLine

                        #region Secure Score Gauge Chart -- generated outside PScribo scope
                        try {
                            if (Get-Command New-AbrSecureScoreGauge -ErrorAction SilentlyContinue) {
                                $script:Charts['SecureScore'] = New-AbrSecureScoreGauge -CurrentScore ([int]$CurrentScore) -MaxScore ([int]$MaxScore) -TenantId $TenantId
                            }
                        } catch { Write-AbrDebugLog "SecureScore chart failed: $($_.Exception.Message)" 'WARN' 'CHART' }
                        if ($script:Charts['SecureScore']) {
                            Image -Text 'Identity Secure Score Gauge' -Base64 $script:Charts['SecureScore'] -Percent 65 -Align Center
                            Paragraph "Figure: Identity Secure Score -- $CurrentScore of $MaxScore points ($ScorePct%)"
                            BlankLine
                        }
                        #endregion

                        $SecScoreObj = [System.Collections.ArrayList]::new()
                        $ssInObj = [ordered] @{
                            'Current Score'        = "$CurrentScore / $MaxScore"
                            'Score Percentage'     = "$ScorePct%"
                            'Score Date'           = $ScoreDate
                            'Benchmark'            = if ($ScorePct -ge 80) { '[OK] Good -- above 80%' }
                                                     elseif ($ScorePct -ge 50) { '[WARN] Moderate -- 50-79%' }
                                                     else { '[FAIL] Low -- below 50%. Review Improvement Actions.' }
                        }
                        $SecScoreObj.Add([pscustomobject]$ssInObj) | Out-Null
                        $null = (& { if ($HealthCheck.EntraID.TenantOverview) {
                            $null = ($SecScoreObj | Where-Object { $_.'Score Percentage' -like '*FAIL*' } | Set-Style -Style Critical | Out-Null)
                            $null = ($SecScoreObj | Where-Object { $_.'Score Percentage' -like '*WARN*' } | Set-Style -Style Warning  | Out-Null)
                        }})
                        $SSTableParams = @{ Name = "Identity Secure Score - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                        if ($Report.ShowTableCaptions) { $SSTableParams['Caption'] = "- $($SSTableParams.Name)" }
                        $SecScoreObj | Table @SSTableParams

                        # Top improvement actions
                        $ControlProfiles = $Score.controlScores | Sort-Object { $_.controlScore } | Select-Object -First 8
                        if ($ControlProfiles) {
                            BlankLine
                            Paragraph 'Top Improvement Actions (lowest scoring controls):'
                            BlankLine
                            $ImprovObj = [System.Collections.ArrayList]::new()
                            foreach ($ctrl in $ControlProfiles) {
                                $null = $ImprovObj.Add([pscustomobject][ordered]@{
                                    'Control'       = $ctrl.controlName
                                    'Score'         = "$([math]::Round($ctrl.controlScore,1)) / $([math]::Round($ctrl.maxScore,1))"
                                    'Description'   = if ($ctrl.description) {
                                        $cleanDesc = [regex]::Replace($ctrl.description, '<[^>]+>', [string]::Empty)
                                        if ($cleanDesc.Length -le 120) { $cleanDesc } else {
                                            $truncated = $cleanDesc.Substring(0, 120)
                                            $lastSpace = $truncated.LastIndexOf(' ')
                                            if ($lastSpace -gt 80) { $truncated.Substring(0, $lastSpace) + '...' } else { $truncated + '...' }
                                        }
                                    } else { '--' }
                                })
                            }
                            $ImprovTableParams = @{ Name = "Secure Score Improvement Actions - $TenantId"; List = $false; ColumnWidths = 28, 14, 58 }
                            if ($Report.ShowTableCaptions) { $ImprovTableParams['Caption'] = "- $($ImprovTableParams.Name)" }
                            $ImprovObj | Table @ImprovTableParams
                        }
                        $null = ($script:ExcelSheets['Secure Score'] = $SecScoreObj)

                    }
                }
            } catch {
                Write-AbrDebugLog "Identity Secure Score unavailable: $($_.Exception.Message)" 'WARN' 'OVERVIEW'
            }
            #endregion

        } # end Section Tenant Overview
        #endregion
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Tenant Overview'
    }
}