Src/Private/Get-AbrEntraIDGovernance.ps1
|
function Get-AbrEntraIDGovernance { [CmdletBinding()] param ([Parameter(Position=0,Mandatory)][string]$TenantId) begin { Write-PScriboMessage "Collecting Identity Governance data for tenant $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Governance' } process { Section -Style Heading1 'Identity Governance' { Paragraph "The following section documents the Identity Governance configuration for tenant $TenantId, including Access Reviews, Entitlement Management, and Terms of Use." BlankLine #region Access Reviews Section -Style Heading2 'Access Reviews' { Paragraph "Access Reviews allow periodic verification that users still require the access they have been granted. They are a key control for privileged role hygiene and guest access management." BlankLine try { Write-Host " - Retrieving Access Review definitions..." $ReviewsResp = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions?$top=999' ` -ErrorAction Stop $AllReviews = $ReviewsResp.value while ($ReviewsResp.'@odata.nextLink') { $ReviewsResp = Invoke-MgGraphRequest -Method GET -Uri $ReviewsResp.'@odata.nextLink' -ErrorAction Stop $AllReviews += $ReviewsResp.value } if ($AllReviews -and @($AllReviews).Count -gt 0) { $ActiveReviews = @($AllReviews | Where-Object { $_.status -eq 'InProgress' }).Count $CompletedReviews = @($AllReviews | Where-Object { $_.status -eq 'Completed' }).Count $RecurringReviews = @($AllReviews | Where-Object { $_.settings.recurrence -ne $null }).Count $ARSumObj = [System.Collections.ArrayList]::new() $arSumInObj = [ordered] @{ 'Total Access Review Definitions' = @($AllReviews).Count 'Active (In Progress)' = $ActiveReviews 'Completed' = $CompletedReviews 'Recurring Reviews' = $RecurringReviews } $ARSumObj.Add([pscustomobject]$arSumInObj) | Out-Null $ARSumTableParams = @{ Name = "Access Review Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $ARSumTableParams['Caption'] = "- $($ARSumTableParams.Name)" } $ARSumObj | Table @ARSumTableParams if ($InfoLevel.Governance -ge 2) { $ARDetailObj = [System.Collections.ArrayList]::new() foreach ($Review in ($AllReviews | Sort-Object displayName | Select-Object -First 50)) { $arInObj = [ordered] @{ 'Review Name' = $Review.displayName 'Status' = $Review.status 'Scope' = if ($Review.scope.query) { $Review.scope.query } else { '--' } 'Recurring' = if ($Review.settings.recurrence) { 'Yes' } else { 'No' } 'Auto-Apply' = if ($Review.settings.autoApplyDecisionsEnabled) { 'Yes' } else { 'No' } 'Created' = if ($Review.createdDateTime) { ([datetime]$Review.createdDateTime).ToString('yyyy-MM-dd') } else { '--' } } $ARDetailObj.Add([pscustomobject]$arInObj) | Out-Null } $null = (& { if ($HealthCheck.EntraID.Roles) { $null = ($ARDetailObj | Where-Object { $_.'Auto-Apply' -eq 'No' } | Set-Style -Style Warning | Out-Null) } }) $ARDetailTableParams = @{ Name = "Access Review Definitions - $TenantId"; List = $false; ColumnWidths = 24, 12, 26, 10, 10, 12 } if ($Report.ShowTableCaptions) { $ARDetailTableParams['Caption'] = "- $($ARDetailTableParams.Name)" } $ARDetailObj | Table @ARDetailTableParams $null = ($script:ExcelSheets['Access Reviews'] = $ARDetailObj) } $null = ($script:ExcelSheets['Access Review Summary'] = $ARSumObj) } else { Paragraph "[FAIL] No Access Reviews are configured in tenant $TenantId. Access Reviews are required to periodically verify that privileged role assignments and guest access are still appropriate." $null = (& { if ($HealthCheck.EntraID.Roles) { # placeholder paragraph already written above }}) } } catch { Write-AbrSectionError -Section 'Access Reviews' -Message "Access Reviews require IdentityGovernance licence (Entra ID P2 or Governance). Error: $($_.Exception.Message)" } # ACSC + CIS BlankLine $HasReviews = ($AllReviews -and @($AllReviews).Count -gt 0) # Privileged role reviews detection $HasPrivRoleReviews = $false $PrivRoleReviewDetail = 'No access reviews found targeting privileged directory roles.' if ($HasReviews) { foreach ($Rev in $AllReviews) { $isRoleReview = $false try { $scope = $Rev.Scope.AdditionalProperties if ($scope -and ($scope['principalScopes'] -or $scope['resourceScopes'])) { $isRoleReview = $true } } catch {} if ($Rev.DisplayName -match 'admin|role|privileged|global|GA') { $isRoleReview = $true } if ($isRoleReview) { $HasPrivRoleReviews = $true; break } } if ($HasPrivRoleReviews) { $PrivRoleReviewDetail = "Privileged role review(s) detected. Verify targets include Global Admin and other sensitive roles. [OK]" } else { $PrivRoleReviewDetail = "No reviews appear to target privileged roles. Create access reviews for Global Admin, Privileged Role Admin, etc. [WARN]" } } # PIM approval checks for GA and PRA $GAApprovalRequired = $false; $GAApprovalState = 'Not verified (PIM data unavailable)' $PRAApprovalRequired = $false; $PRAApprovalState = 'Not verified (PIM data unavailable)' try { $GARoleId = '62e90394-69f5-4237-9190-012177145e10' # Global Administrator $PRARoleId = 'e8611ab8-c189-46e8-94e1-60213ab1f814' # Privileged Role Administrator foreach ($RoleId in @($GARoleId, $PRARoleId)) { $PolicyAssign = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicyAssignments?`$filter=scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '$RoleId'" ` -ErrorAction SilentlyContinue if ($PolicyAssign -and $PolicyAssign.value -and $PolicyAssign.value.Count -gt 0) { $PolicyId = $PolicyAssign.value[0].policyId $Rules = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicies/$PolicyId/rules" ` -ErrorAction SilentlyContinue if ($Rules -and $Rules.value) { $ApprovalRule = $Rules.value | Where-Object { $_.'@odata.type' -like '*approvalRule*' } $IsRequired = ($ApprovalRule -and $ApprovalRule.setting.isApprovalRequired -eq $true) if ($RoleId -eq $GARoleId) { $GAApprovalRequired = $IsRequired; $GAApprovalState = if ($IsRequired) { 'Approval required [OK]' } else { 'No approval required [WARN]' } } if ($RoleId -eq $PRARoleId) { $PRAApprovalRequired = $IsRequired; $PRAApprovalState = if ($IsRequired) { 'Approval required [OK]' } else { 'No approval required [WARN]' } } } } } } catch { Write-AbrDebugLog "PIM approval check skipped: $($_.Exception.Message)" 'WARN' 'GOVERNANCE' } if ($script:IncludeACSCe8) { Paragraph "ACSC Essential Eight Maturity Level Assessment -- Access Reviews:" BlankLine #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json) $_ComplianceVars = @{ 'HasReviews' = $HasReviews 'RecurringReviews' = $RecurringReviews 'ReviewCount' = $ReviewCount 'HasPrivRoleReviews' = $HasPrivRoleReviews 'PrivRoleReviewDetail'= $PrivRoleReviewDetail 'GAApprovalRequired' = $GAApprovalRequired 'GAApprovalState' = $GAApprovalState 'PRAApprovalRequired' = $PRAApprovalRequired 'PRAApprovalState' = $PRAApprovalState } $E8ARChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrE8Checks -Section 'Governance') ` -Framework E8 ` -CallerVariables $_ComplianceVars New-AbrE8AssessmentTable -Checks $E8ARChecks -Name 'Access Reviews' -TenantId $TenantId # Consolidated into ACSC E8 Assessment sheet if ($E8ARChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8ARChecks | Select-Object @{N='Section';E={'Governance'}}, ML, Control, Status, Detail ))) } #endregion } if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Access Reviews:" BlankLine #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json) $CISARChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrCISChecks -Section 'Governance') ` -Framework CIS ` -CallerVariables $_ComplianceVars New-AbrCISAssessmentTable -Checks $CISARChecks -Name 'Access Reviews' -TenantId $TenantId # Consolidated into CIS Assessment sheet if ($CISARChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISARChecks | Select-Object @{N='Section';E={'Governance'}}, CISControl, Level, Status, Detail ))) } #endregion } } #endregion #region Entitlement Management Section -Style Heading2 'Entitlement Management' { Paragraph "Entitlement Management provides access packages for structured, time-limited access to resources. It is an Entra ID P2 / Governance feature." BlankLine try { Write-Host " - Retrieving Entitlement Management access packages..." $APResp = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/accessPackages?$top=999' ` -ErrorAction Stop $AllPackages = $APResp.value if ($AllPackages -and @($AllPackages).Count -gt 0) { $APObj = [System.Collections.ArrayList]::new() foreach ($AP in ($AllPackages | Sort-Object displayName | Select-Object -First 50)) { $apInObj = [ordered] @{ 'Package Name' = $AP.displayName 'Description' = if ($AP.description) { $AP.description.Substring(0,[Math]::Min(80,$AP.description.Length)) } else { '--' } 'Hidden' = if ($AP.isHidden) { 'Yes' } else { 'No' } 'Created' = if ($AP.createdDateTime) { ([datetime]$AP.createdDateTime).ToString('yyyy-MM-dd') } else { '--' } } $APObj.Add([pscustomobject]$apInObj) | Out-Null } $APTableParams = @{ Name = "Access Packages - $TenantId"; List = $false; ColumnWidths = 24, 46, 10, 12 } if ($Report.ShowTableCaptions) { $APTableParams['Caption'] = "- $($APTableParams.Name)" } $APObj | Table @APTableParams $null = ($script:ExcelSheets['Access Packages'] = $APObj) Paragraph "$(@($AllPackages).Count) access package(s) configured. Entitlement Management provides structured, policy-governed access to groups, apps, and SharePoint sites with automatic expiry and access reviews." } else { Paragraph "[INFO] No Access Packages are configured. Entitlement Management access packages provide structured, time-limited access with automatic expiry — recommended for ML3 privileged access management." } } catch { Write-AbrSectionError -Section 'Entitlement Management' -Message "Entitlement Management requires Entra ID Governance licence. Error: $($_.Exception.Message)" } } #endregion #region Terms of Use Section -Style Heading2 'Terms of Use' { Paragraph "Terms of Use policies require users to accept usage terms before accessing resources. They can be enforced via Conditional Access." BlankLine try { Write-Host " - Retrieving Terms of Use policies..." $TOUResp = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/agreements' ` -ErrorAction Stop $AllTOU = $TOUResp.value if ($AllTOU -and @($AllTOU).Count -gt 0) { $TOUObj = [System.Collections.ArrayList]::new() foreach ($TOU in ($AllTOU | Sort-Object displayName)) { $touInObj = [ordered] @{ 'Name' = $TOU.displayName 'Re-acceptance Required' = if ($TOU.isPerDeviceAcceptanceRequired) { 'Per device' } elseif ($TOU.isViewingBeforeAcceptanceRequired) { 'Must view before accept' } else { 'Standard' } 'Expiry After Acceptance' = if ($TOU.termsExpiration) { $TOU.termsExpiration.frequency } else { 'No expiry' } 'Enforced via CA' = '[INFO] Check Conditional Access policies for TermsOfUse grant control' } $TOUObj.Add([pscustomobject]$touInObj) | Out-Null } $TOUTableParams = @{ Name = "Terms of Use Policies - $TenantId"; List = $false; ColumnWidths = 25, 22, 20, 33 } if ($Report.ShowTableCaptions) { $TOUTableParams['Caption'] = "- $($TOUTableParams.Name)" } $TOUObj | Table @TOUTableParams $null = ($script:ExcelSheets['Terms of Use'] = $TOUObj) } else { Paragraph "[INFO] No Terms of Use policies are configured in tenant $TenantId. Terms of Use can enforce acceptable use acknowledgement for specific user groups or external/guest users." } } catch { Write-AbrSectionError -Section 'Terms of Use' -Message "$($_.Exception.Message)" } } #endregion } # end Section Governance } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Governance' } } |