public/maester/entra/Test-MtEntitlementManagementValidApprovers.ps1

<#
.SYNOPSIS
    Checks if access package approval workflows have valid approvers

.DESCRIPTION
    MT.1109 - Access package approval workflows must have valid approvers

    This test identifies Microsoft Entra ID Governance access package assignment policies with
    approval workflows that reference invalid approvers. Invalid approvers can cause:
    - Approval workflow failures
    - Access request timeouts
    - Broken automation flows
    - User frustration and support tickets

    The test validates that all approval workflows have:
    - Valid user approvers (account enabled, not deleted)
    - Valid group approvers (group exists and has members)
    - Manager approvers where requestor has an assigned manager
    - No references to deleted or disabled accounts

    Learn more:
    https://maester.dev/docs/tests/MT.1109

.EXAMPLE
    Test-MtEntitlementManagementValidApprovers

    Returns $true if all approval workflows have valid approvers

.LINK
    https://maester.dev/docs/commands/Test-MtEntitlementManagementValidApprovers
#>


function Test-MtEntitlementManagementValidApprovers {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Approvers is the resource type being tested')]
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    try {
        # Get all access packages
        $accessPackages = Invoke-MtGraphRequest -RelativeUri "identityGovernance/entitlementManagement/accessPackages" -ApiVersion beta

        $packages = @()
        if ($accessPackages -is [Array]) {
            $packages = $accessPackages
        } elseif ($null -ne $accessPackages.value) {
            $packages = $accessPackages.value
        } elseif ($null -ne $accessPackages) {
            $packages = @($accessPackages)
        }

        if ($packages.Count -eq 0) {
            $testResult = "✅ No access packages found in the tenant."
            Add-MtTestResultDetail -Result $testResult
            return $true
        }

        $invalidApproversFound = @()

        # Check each access package for invalid approvers
        foreach ($package in $packages) {
            $packageId = if ($package.id) { $package.id } else { $package.PSObject.Properties['id'].Value }

            if ([string]::IsNullOrEmpty($packageId)) {
                Write-Verbose "Skipping package without ID: $($package.displayName)"
                continue
            }

            $packageName = if ($package.displayName) { $package.displayName } else { $package.PSObject.Properties['displayName'].Value }
            Write-Verbose "Checking access package: $packageName (ID: $packageId)"

            # Get assignment policies
            try {
                $policies = Invoke-MtGraphRequest -RelativeUri "identityGovernance/entitlementManagement/accessPackageAssignmentPolicies?`$filter=accessPackage/id eq '$packageId'" -ApiVersion beta

                $policyArray = @()
                if ($policies -is [Array]) {
                    $policyArray = $policies
                } elseif ($null -ne $policies.value) {
                    $policyArray = $policies.value
                } elseif ($null -ne $policies) {
                    $policyArray = @($policies)
                }

                if ($policyArray.Count -eq 0) {
                    Write-Verbose "No policies found for package: $packageName"
                    continue
                }

                # Check each policy for approval workflow issues
                foreach ($policy in $policyArray) {
                    $policyName = if ($policy.displayName) { $policy.displayName } else { $policy.PSObject.Properties['displayName'].Value }

                    # Skip default system policies
                    if ($policyName -like "*All members*" -and $policyName -like "*excluding guests*") {
                        Write-Verbose "Skipping default system policy: $policyName"
                        continue
                    }

                    Write-Verbose "Checking policy: $policyName"

                    $requestApprovalSettings = $policy.requestApprovalSettings
                    if ($null -eq $requestApprovalSettings) {
                        Write-Verbose "Policy has no approval settings"
                        continue
                    }

                    $isApprovalRequired = $requestApprovalSettings.isApprovalRequired
                    if (-not $isApprovalRequired) {
                        Write-Verbose "Policy does not require approval"
                        continue
                    }

                    $approvalStages = $requestApprovalSettings.approvalStages
                    if ($null -eq $approvalStages -or $approvalStages.Count -eq 0) {
                        $invalidApproversFound += [PSCustomObject]@{
                            PackageId = $packageId
                            PackageName = $packageName
                            PolicyName = $policyName
                            Issue = "No approval stages"
                            ApproverType = "N/A"
                            ApproverDetails = "Approval required but no stages defined"
                        }
                        continue
                    }

                    # Check each approval stage
                    foreach ($stage in $approvalStages) {
                        $primaryApprovers = $stage.primaryApprovers
                        if ($null -eq $primaryApprovers -or $primaryApprovers.Count -eq 0) {
                            $invalidApproversFound += [PSCustomObject]@{
                                PackageId = $packageId
                                PackageName = $packageName
                                PolicyName = $policyName
                                Issue = "No primary approvers"
                                ApproverType = "N/A"
                                ApproverDetails = "Stage has no approvers"
                            }
                            continue
                        }

                        # Check each approver
                        foreach ($approver in $primaryApprovers) {
                            $approverType = $approver.'@odata.type'

                            switch ($approverType) {
                                '#microsoft.graph.singleUser' {
                                    $userId = if ($approver.userId) { $approver.userId } elseif ($approver.id) { $approver.id } else {
                                        if ($approver.PSObject.Properties['userId']) { $approver.PSObject.Properties['userId'].Value } else { $approver.PSObject.Properties['id'].Value }
                                    }

                                    if ([string]::IsNullOrEmpty($userId)) {
                                        $invalidApproversFound += [PSCustomObject]@{
                                            PackageId = $packageId
                                            PackageName = $packageName
                                            PolicyName = $policyName
                                            Issue = "User has no ID"
                                            ApproverType = "User"
                                            ApproverDetails = "Invalid configuration"
                                        }
                                        continue
                                    }

                                    try {
                                        $user = Invoke-MtGraphRequest -RelativeUri "users/$userId" -ApiVersion beta -ErrorAction SilentlyContinue

                                        if ($null -eq $user) {
                                            $invalidApproversFound += [PSCustomObject]@{
                                                PackageId = $packageId
                                                PackageName = $packageName
                                                PolicyName = $policyName
                                                Issue = "User not found"
                                                ApproverType = "User"
                                                ApproverDetails = "ID: $userId"
                                            }
                                        } elseif ($user.accountEnabled -eq $false) {
                                            $userName = if ($user.displayName) { $user.displayName } else { $user.userPrincipalName }
                                            $invalidApproversFound += [PSCustomObject]@{
                                                PackageId = $packageId
                                                PackageName = $packageName
                                                PolicyName = $policyName
                                                Issue = "User disabled"
                                                ApproverType = "User"
                                                ApproverDetails = "$userName"
                                            }
                                        }
                                    } catch {
                                        if ($_.Exception.Message -like "*404*" -or $_.Exception.Message -like "*not found*") {
                                            $invalidApproversFound += [PSCustomObject]@{
                                                PackageId = $packageId
                                                PackageName = $packageName
                                                PolicyName = $policyName
                                                Issue = "User deleted"
                                                ApproverType = "User"
                                                ApproverDetails = "ID: $userId"
                                            }
                                        }
                                    }
                                }

                                '#microsoft.graph.groupMembers' {
                                    $groupId = if ($approver.groupId) { $approver.groupId } elseif ($approver.id) { $approver.id } else {
                                        if ($approver.PSObject.Properties['groupId']) { $approver.PSObject.Properties['groupId'].Value } else { $approver.PSObject.Properties['id'].Value }
                                    }

                                    if ([string]::IsNullOrEmpty($groupId)) {
                                        $invalidApproversFound += [PSCustomObject]@{
                                            PackageId = $packageId
                                            PackageName = $packageName
                                            PolicyName = $policyName
                                            Issue = "Group has no ID"
                                            ApproverType = "Group"
                                            ApproverDetails = "Invalid configuration"
                                        }
                                        continue
                                    }

                                    try {
                                        $group = Invoke-MtGraphRequest -RelativeUri "groups/$groupId" -ApiVersion beta -ErrorAction SilentlyContinue

                                        if ($null -eq $group) {
                                            $invalidApproversFound += [PSCustomObject]@{
                                                PackageId = $packageId
                                                PackageName = $packageName
                                                PolicyName = $policyName
                                                Issue = "Group not found"
                                                ApproverType = "Group"
                                                ApproverDetails = "ID: $groupId"
                                            }
                                            continue
                                        }

                                        # Check if group has members
                                        try {
                                            $members = Invoke-MtGraphRequest -RelativeUri "groups/$groupId/members?`$top=1" -ApiVersion beta -ErrorAction SilentlyContinue

                                            $memberCount = 0
                                            if ($members -is [Array]) {
                                                $memberCount = $members.Count
                                            } elseif ($null -ne $members.value) {
                                                $memberCount = $members.value.Count
                                            } elseif ($null -ne $members) {
                                                $memberCount = 1
                                            }

                                            if ($memberCount -eq 0) {
                                                $groupName = if ($group.displayName) { $group.displayName } else { "Unknown" }
                                                $invalidApproversFound += [PSCustomObject]@{
                                                    PackageId = $packageId
                                                    PackageName = $packageName
                                                    PolicyName = $policyName
                                                    Issue = "Group has no members"
                                                    ApproverType = "Group"
                                                    ApproverDetails = $groupName
                                                }
                                            }
                                        } catch {
                                            Write-Verbose "Error checking members for group $groupId"
                                        }
                                    } catch {
                                        if ($_.Exception.Message -like "*404*" -or $_.Exception.Message -like "*not found*") {
                                            $invalidApproversFound += [PSCustomObject]@{
                                                PackageId = $packageId
                                                PackageName = $packageName
                                                PolicyName = $policyName
                                                Issue = "Group deleted"
                                                ApproverType = "Group"
                                                ApproverDetails = "ID: $groupId"
                                            }
                                        }
                                    }
                                }

                                '#microsoft.graph.requestorManager' {
                                    Write-Verbose "Policy uses manager approval"
                                }

                                '#microsoft.graph.internalSponsors' {
                                    Write-Verbose "Policy uses internal sponsors"
                                }

                                '#microsoft.graph.externalSponsors' {
                                    Write-Verbose "Policy uses external sponsors"
                                }
                            }
                        }
                    }
                }
            } catch {
                Write-Verbose "Error processing package $packageName : $_"
            }
        }

        # Determine test result
        if ($invalidApproversFound.Count -eq 0) {
            $testResult = "✅ All approval workflows have valid approvers.`n`nChecked $($packages.Count) access package(s)."
            Add-MtTestResultDetail -Result $testResult
            return $true
        } else {
            $groupedByPackage = $invalidApproversFound | Group-Object -Property PackageId

            $testResult = "❌ Found $($invalidApproversFound.Count) invalid approver(s) across $($groupedByPackage.Count) access package(s):`n`n"

            $testResult += "| Access Package | Policy | Issue | Type | Details |`n"
            $testResult += "|---|---|---|---|---|`n"

            foreach ($item in $invalidApproversFound) {
                $packageLink = "https://portal.azure.com/#view/Microsoft_Azure_ELMAdmin/EntitlementMenuBlade/~/overview/entitlementId/$($item.PackageId)"
                $packageName = "[$($item.PackageName)]($packageLink)"

                $testResult += "| $packageName | $($item.PolicyName) | $($item.Issue) | $($item.ApproverType) | $($item.ApproverDetails) |`n"
            }

            $testResult += "`n**Remediation:** Update approval workflows to use valid, active approvers in the [Entra portal](https://portal.azure.com/#view/Microsoft_Azure_ELMAdmin).`n"

            Add-MtTestResultDetail -Result $testResult
            return $false
        }

    } catch {
        Write-Error "Error running test: $($_.Exception.Message)"
        return $false
    }
}