Skip to content
Merged
7 changes: 6 additions & 1 deletion src/powershell/tests/Test-Assessment.21879.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
...
## Overview

Without enforced approval on entitlement management policies that allow external users, a threat actor can self-orchestrate initial access by submitting unattended requests that are auto-approved. Each successful request provisions or reuses a guest user object and grant access to resources included in the access package, immediately expanding reconnaissance surface. From that foothold the actor can enumerate additional collaboration surfaces, harvest shared files, and probe mis-scoped app permissions to escalate (e.g., abusing over-privileged group-based roles or app role assignments). They can persist by requesting multiple packages with overlapping or escalating privileges, re-extending assignments if expiration or reviews are lax, or by creating indirect sharing links inside granted SharePoint or Teams resources. Absence of an approval gate also removes a human anomaly check (sponsor/internal/external approver) that would otherwise filter suspicious volume, timing, geography, or improbable justification patterns, shrinking detection dwell-time. This accelerates lateral movement (pivoting through granted group memberships to additional workloads), facilitates data staging and exfiltration from SharePoint/Teams or app APIs, and increases the blast radius before downstream controls (access reviews, expirations) eventually trigger. Microsoft’s guidance explicitly states approval should be required when external users can request access to ensure oversight; bypassing it effectively converts a governed onboarding path into an unsupervised provisioning for external identities, expanding tenant-wide risk until manual discovery or periodic governance cycles intervene.

**Remediation action**

- [Configure approval for external request policies (toggle Require approval)](https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-approval-policy )


<!--- Results --->
%TestResult%
119 changes: 100 additions & 19 deletions src/powershell/tests/Test-Assessment.21879.ps1
Original file line number Diff line number Diff line change
@@ -1,35 +1,116 @@
<#
.SYNOPSIS

Checks if all entitlement management policies that apply to external users require approval
#>

function Test-Assessment-21879{
[ZtTest(
Category = 'Access control',
ImplementationCost = 'Medium',
Pillar = 'Identity',
RiskLevel = 'Medium',
SfiPillar = 'Protect identities and secrets',
TenantType = ('Workforce','External'),
TestId = 21879,
Title = 'All entitlement management policies that apply to External users require approval',
UserImpact = 'Medium'
Category = 'Access control',
ImplementationCost = 'Medium',
Pillar = 'Identity',
RiskLevel = 'Medium',
SfiPillar = 'Protect identities and secrets',
TenantType = ('Workforce','External'),
TestId = 21879,
Title = 'All entitlement management policies that apply to External users require approval',
UserImpact = 'Medium'
)]
[CmdletBinding()]
param()

Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

$activity = "Checking All entitlement management policies that apply to external users require approval"
Write-ZtProgress -Activity $activity -Status "Getting policy"
$activity = 'Checking All entitlement management policies that apply to external users require approval'
Write-ZtProgress -Activity $activity -Status 'Getting assignment policies'

try {
# Query assignment policies directly with expanded access package information (more efficient than expanding from access packages)
$assignmentPolicies = Invoke-ZtGraphRequest -RelativeUri 'identityGovernance/entitlementManagement/assignmentPolicies' -QueryParameters @{'$expand' = 'accessPackage'} -ApiVersion v1.0

# Handle case where no policies exist or API returns null
if ($null -eq $assignmentPolicies -or $assignmentPolicies.Count -eq 0) {
Write-PSFMessage 'No assignment policies found in the tenant' -Level Verbose
$testResultMarkdown = 'No access package assignment policies found in the tenant.'
$passed = $true
}
else {
Write-ZtProgress -Activity $activity -Status 'Filtering policies for external users'

# Client-side filter for policies that apply to external users
$externalUserPolicies = @()

foreach ($policy in $assignmentPolicies) {
# Skip if requestorSettings is null or missing
if ($null -eq $policy.requestorSettings) {
Write-PSFMessage "Skipping policy $($policy.id) - no requestorSettings" -Level Debug
continue
}

# Check if policy accepts requests
if ($policy.requestorSettings.acceptRequests -eq $true) {
# Use the existing Test-ZtExternalUserScope function to check if policy applies to external users
$appliesToExternal = Test-ZtExternalUserScope -TargetScope $policy.allowedTargetScope

if ($appliesToExternal) {
$externalUserPolicies += [PSCustomObject]@{
AccessPackageId = $policy.accessPackage.id
AccessPackageName = $policy.accessPackage.displayName
AssignmentPolicyId = $policy.id
AssignmentPolicyName = $policy.displayName
AssignmentPolicyScopeType = $policy.allowedTargetScope
IsApprovalRequired = [bool]($policy.requestorSettings.approvalSettings.isApprovalRequired)
}
}
}
}

Write-ZtProgress -Activity $activity -Status 'Evaluating approval requirements'

if ($externalUserPolicies.Count -eq 0) {
$testResultMarkdown = 'No access package assignment policies found that apply to external users.'
$passed = $true
}
else {
# Pass/Fail Logic: If there is at least one result where isApprovalRequired == "false", fail the test. Else pass the test
$policiesWithoutApproval = $externalUserPolicies | Where-Object { $_.IsApprovalRequired -eq $false }

if ($policiesWithoutApproval.Count -eq 0) {
# PASS: No policies without approval
$passed = $true
$testResultMarkdown = 'All access package assignment policies for external users require approval'
}
else {
# FAIL: At least one policy without approval
$passed = $false
$testResultMarkdown = 'Access package assignment policies for external users without approval are found in the tenant'
}

# Details: For each policy found
$testResultMarkdown += "`n`n## Details`n`n"
$testResultMarkdown += "Assignment policies that apply to external users:`n`n"
$testResultMarkdown += '| Access package ID | Access package name | Assignment policy ID | Assignment policy Name | Assignment policy scopeType | Assignment policy isApprovalRequired |`n'
$testResultMarkdown += '|:---|:---|:---|:---|:---|:---|`n'

foreach ($policy in $externalUserPolicies) {
$packageLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ERM/DashboardBlade/~/elmEntitlementManagement/menuId/AccessPackages"
$approvalStatus = if ($policy.IsApprovalRequired) { 'true' } else { 'false' }

$result = $false
$testResultMarkdown = "Planned for future release."
$passed = $result
$testResultMarkdown += "| ``$($policy.AccessPackageId)`` | [$(Get-SafeMarkdown $policy.AccessPackageName)]($packageLink) | ``$($policy.AssignmentPolicyId)`` | $(Get-SafeMarkdown $policy.AssignmentPolicyName) | $($policy.AssignmentPolicyScopeType) | **$approvalStatus** |`n"
}
}
}
}
catch {
Write-PSFMessage "Error querying entitlement management: $($_.Exception.Message)" -Level Warning
$testResultMarkdown = 'Unable to query entitlement management policies. This may indicate that Entitlement Management is not enabled or configured in the tenant, or insufficient permissions to access the data.'
$passed = $false
}

$params = @{
TestId = '21879'
Status = $passed
Result = $testResultMarkdown
}

Add-ZtTestResultDetail -TestId '21879' -Title "All entitlement management policies that apply to external users require approval" `
-UserImpact Medium -Risk Medium -ImplementationCost Medium `
-AppliesTo Identity -Tag Identity `
-Status $passed -Result $testResultMarkdown -SkippedBecause UnderConstruction
Add-ZtTestResultDetail @params
}