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.21 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' } } |