public/maester/entra/Test-MtEntitlementManagementInactivePolicies.ps1

<#
.SYNOPSIS
    Checks if access packages have inactive or orphaned assignment policies

.DESCRIPTION
    MT.1108 - Access packages should not reference inactive or orphaned assignment policies

    This test identifies Microsoft Entra ID Governance access packages that contain assignment policies
    which are disabled, misconfigured, or orphaned. Inactive policies can cause:
    - Blocked access requests
    - Broken approval workflows
    - Inconsistent user lifecycle automation
    - Configuration drift

    The test validates that all assignment policies are:
    - Accepting requests (requestorSettings.acceptRequests = true)
    - Properly configured with valid scope types
    - Not using deprecated scope types (e.g., "NoSubjects")
    - Have valid approval settings where required
    - Not expired
    - Have proper question configuration

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

.EXAMPLE
    Test-MtEntitlementManagementInactivePolicies

    Returns $true if all access package assignment policies are active and properly configured

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


function Test-MtEntitlementManagementInactivePolicies {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Policies 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
        }

        $inactivePoliciesFound = @()

        # Check each access package for inactive or misconfigured policies
        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 access package 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
                }

                foreach ($policy in $policyArray) {
                    $policyId = if ($policy.id) { $policy.id } else { $policy.PSObject.Properties['id'].Value }
                    $policyName = if ($policy.displayName) { $policy.displayName } else {
                        if ($policy.PSObject.Properties['displayName']) { $policy.PSObject.Properties['displayName'].Value } else { "Unnamed Policy" }
                    }

                    Write-Verbose "Checking policy: $policyName (ID: $policyId)"

                    $issues = @()

                    # Check 1: Validate if policy accepts requests
                    if ($policy.requestorSettings.acceptRequests -eq $false) {
                        $issues += "Policy is not accepting new requests"
                    }

                    # Check 2: Validate requestor scope type
                    if ($policy.requestorSettings) {
                        $scopeType = $policy.requestorSettings.scopeType

                        if ([string]::IsNullOrEmpty($scopeType)) {
                            $issues += "Requestor scope type is missing"
                        } elseif ($scopeType -eq "NoSubjects") {
                            $issues += "Nobody can request access (NoSubjects)"
                        } elseif ($scopeType -eq "SpecificDirectorySubjects") {
                            $allowedRequestors = $policy.requestorSettings.allowedRequestors
                            if ($null -eq $allowedRequestors -or $allowedRequestors.Count -eq 0) {
                                $issues += "No users/groups allowed to request"
                            }
                        }
                    } else {
                        $issues += "Requestor settings are missing"
                    }

                    # Check 3: Validate approval settings if approval is required
                    if ($policy.requestApprovalSettings) {
                        $isApprovalRequired = $policy.requestApprovalSettings.isApprovalRequired

                        if ($isApprovalRequired -eq $true) {
                            $approvalStages = $policy.requestApprovalSettings.approvalStages

                            if ($null -eq $approvalStages -or $approvalStages.Count -eq 0) {
                                $issues += "Approval required but no stages configured"
                            } else {
                                $hasValidApprovers = $false
                                foreach ($stage in $approvalStages) {
                                    if ($stage.primaryApprovers -and $stage.primaryApprovers.Count -gt 0) {
                                        $hasValidApprovers = $true
                                        break
                                    }
                                }

                                if (-not $hasValidApprovers) {
                                    $issues += "No valid approvers configured"
                                }
                            }
                        }
                    }

                    # Check 4: Validate expiration settings
                    if ($policy.PSObject.Properties['expirationDateTime']) {
                        $expirationDate = $policy.PSObject.Properties['expirationDateTime'].Value
                        if ($null -ne $expirationDate) {
                            $expDate = [DateTime]::Parse($expirationDate)
                            if ($expDate -lt (Get-Date)) {
                                $issues += "Policy expired on $($expDate.ToString('yyyy-MM-dd'))"
                            }
                        }
                    }

                    # If any issues found, add to results
                    if ($issues.Count -gt 0) {
                        $inactivePoliciesFound += [PSCustomObject]@{
                            PackageName = $packageName
                            PackageId = $packageId
                            PolicyName = $policyName
                            PolicyId = $policyId
                            ScopeType = $policy.requestorSettings.scopeType
                            Issues = $issues
                        }
                    }
                }
            } catch {
                Write-Verbose "Could not retrieve assignment policies for access package: $packageName. Error: $_"
            }
        }

        # Evaluate results
        $disabledPolicies = $inactivePoliciesFound | Where-Object { $_.ScopeType -ne "Error" }
        $result = $disabledPolicies.Count -eq 0

        if ($result) {
            $testResult = "✅ All access package assignment policies are active and properly configured."
            Add-MtTestResultDetail -Result $testResult
        } else {
            $issuesByPackage = $disabledPolicies | Group-Object PackageId
            $testResult = "❌ Found $($disabledPolicies.Count) inactive policy/policies across $($issuesByPackage.Count) access package(s):`n`n"

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

            foreach ($packageGroup in $issuesByPackage) {
                $packageName = ($packageGroup.Group | Select-Object -First 1).PackageName
                $packageId = $packageGroup.Name
                $packageLink = "https://portal.azure.com/#view/Microsoft_Azure_ELMAdmin/EntitlementMenuBlade/~/overview/entitlementId/$packageId"

                foreach ($policyIssue in $packageGroup.Group) {
                    $primaryIssue = ""
                    if ($policyIssue.Issues -match "NoSubjects") {
                        $primaryIssue = "No one can request"
                    } elseif ($policyIssue.Issues -match "not accepting new requests") {
                        $primaryIssue = "Not accepting requests"
                    } elseif ($policyIssue.Issues -match "No users/groups") {
                        $primaryIssue = "No users allowed"
                    } elseif ($policyIssue.Issues -match "expired") {
                        $primaryIssue = "Expired"
                    } elseif ($policyIssue.Issues -match "no stages|No valid approvers") {
                        $primaryIssue = "No approvers"
                    } else {
                        $primaryIssue = $policyIssue.Issues[0]
                    }

                    $testResult += "| [$packageName]($packageLink) | $($policyIssue.PolicyName) | $primaryIssue |`n"
                }
            }

            $testResult += "`n**Remediation:** Update or remove these policies in the [Entra portal](https://portal.azure.com/#view/Microsoft_Azure_ELMAdmin).`n"

            Add-MtTestResultDetail -Result $testResult
        }

        return $result

    } catch {
        Write-Error "Error checking access package assignment policies: $($_.Exception.Message)"
        return $false
    }
}