tests/Test-Assessment.61009.ps1

<#
.SYNOPSIS
    Conditional Access policies cover both agent identities and agent users.
 
.DESCRIPTION
    This test checks whether at least one enabled Conditional Access policy targets agent identities
    and at least one enabled Conditional Access policy targets agent users, both with
    the 'block' grant control. Both conditions must hold for the check to pass.
 
.NOTES
    Test ID: 61009
    Workshop Task: AI_003
    Pillar: AI
    Category: AI Identity & Access
    Risk Level: High
    Supported Clouds: Global
    Required Permission: Policy.Read.All or Policy.Read.ConditionalAccess
#>


function Test-Assessment-61009 {
    [ZtTest(
        Category = 'AI Identity & Access',
        ImplementationCost = 'Low',
        MinimumLicense = ('AAD_PREMIUM', 'AGENT_365'),
        CompatibleLicense = ('AAD_PREMIUM&AGENT_365', 'AAD_PREMIUM_P2&AGENT_365'),
        Pillar = 'AI',
        Service = ('Graph'),
        RiskLevel = 'High',
        SfiPillar = 'Protect identities and secrets',
        TenantType = ('Workforce'),
        TestId = 61009,
        Title = 'Conditional Access policies cover both agent identities and agent users',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

    $activity = 'Checking Conditional Access coverage for agent identities and agent users'
    Write-ZtProgress -Activity $activity -Status 'Querying Conditional Access policies'

    $queryError = $null

    try {
        $allCAPolicies = Get-ZtConditionalAccessPolicy -ErrorAction Stop
    }
    catch {
        $queryError = $_
        Write-PSFMessage "Failed to retrieve Conditional Access policies: $_" -Tag Test -Level Warning
    }
    #endregion Data Collection

    #region Assessment Logic
    $passed                     = $false
    $customStatus               = $null
    $testResultMarkdown         = ''
    $agentIdentityPolicies      = @()
    $agentUserPolicies          = @()
    $agentIdentityBlockPolicies = @()
    $agentUserBlockPolicies     = @()

    if ($queryError) {
        $testResultMarkdown = "❌ Unable to determine Conditional Access policy configuration for agent principals due to a query failure or insufficient permissions.`n`n**Error:** ``$queryError```n`n%TestResult%"
    }
    else {
        # Policies targeting agent identities via the agent-specific inclusion array.
        $agentIdentityPolicies = @($allCAPolicies | Where-Object {
            $null -ne $_.conditions.clientApplications -and (
                ($null -ne $_.conditions.clientApplications.includeAgentIdServicePrincipals -and
                 @($_.conditions.clientApplications.includeAgentIdServicePrincipals).Count -gt 0)
            )
        })

        # Policies targeting agent users via the agent-specific inclusion array.
        $agentUserPolicies = @($allCAPolicies | Where-Object {
            $null -ne $_.conditions.agents -and
            $null -ne $_.conditions.agents.includeAgentUsers -and
            @($_.conditions.agents.includeAgentUsers).Count -gt 0
        })

        # Enabled agent-identity policies that apply the 'block' grant control
        $agentIdentityBlockPolicies = @($agentIdentityPolicies | Where-Object {
            $_.state -eq 'enabled' -and $_.grantControls.builtInControls -contains 'block'
        })

        # Enabled agent-user policies that apply the 'block' grant control
        $agentUserBlockPolicies = @($agentUserPolicies | Where-Object {
            $_.state -eq 'enabled' -and $_.grantControls.builtInControls -contains 'block'
        })

        $agentIdentityAllPolicies = @($agentIdentityBlockPolicies | Where-Object {
            @($_.conditions.applications.includeApplications) -contains 'All'
        })
        $agentUserAllPolicies = @($agentUserBlockPolicies | Where-Object {
            @($_.conditions.applications.includeApplications) -contains 'All'
        })
        $agentIdentityRestrictedPolicies = @($agentIdentityBlockPolicies | Where-Object {
            @($_.conditions.applications.includeApplications) -contains 'AllAgentIdResources' -and
            -not (@($_.conditions.applications.includeApplications) -contains 'All')
        })
        $agentUserRestrictedPolicies = @($agentUserBlockPolicies | Where-Object {
            @($_.conditions.applications.includeApplications) -contains 'AllAgentIdResources' -and
            -not (@($_.conditions.applications.includeApplications) -contains 'All')
        })

        $agentIdentityOutcome = if ($agentIdentityAllPolicies.Count -ge 1) { 'Pass' }
                                elseif ($agentIdentityRestrictedPolicies.Count -ge 1) { 'Investigate' }
                                else { 'Fail' }
        $agentUserOutcome = if ($agentUserAllPolicies.Count -ge 1) { 'Pass' }
                            elseif ($agentUserRestrictedPolicies.Count -ge 1) { 'Investigate' }
                            else { 'Fail' }

        if ($agentIdentityOutcome -eq 'Fail' -or $agentUserOutcome -eq 'Fail') {
            $passed = $false
            $testResultMarkdown = "❌ Conditional Access policy coverage is insufficient for agent identities, agents' user accounts, or both.`n`n%TestResult%"
        }
        elseif ($agentIdentityOutcome -eq 'Investigate' -or $agentUserOutcome -eq 'Investigate') {
            $passed = $false
            $customStatus = 'Investigate'
            $testResultMarkdown = "⚠️ A required Conditional Access policy uses ``AllAgentIdResources`` instead of ``All``. The CA does not cover all resources.`n`n%TestResult%"
        }
        else {
            $passed = $true
            $testResultMarkdown = "✅ At least one enabled Conditional Access policy blocks all resources for agent identities, and at least one blocks all resources for agents' user accounts.`n`n%TestResult%"
        }
    }
    #endregion Assessment Logic

    #region Report Generation
    $mdInfo              = ''
    $reportTitle         = 'Conditional Access policies covering agent principals'
    $portalLink          = 'https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies'
    $portalPolicyBaseUrl = 'https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/'

    $formatTemplate = @'
 
 
### [{0}]({1})
 
| Policy | Targeted Agent Identity types | State | Grant controls | Status |
| :----- | :---------------------------- | :---- | :------------- | :----- |
{2}
'@


    $agentPolicies = @()

    if (-not $queryError) {
        foreach ($policy in ($agentIdentityPolicies | Sort-Object displayName)) {
            $clientApps   = $policy.conditions.clientApplications
            $targetedType = if ($null -ne $clientApps -and
                                $clientApps.includeAgentIdServicePrincipals -contains 'All' -and
                                ($null -eq $clientApps.excludeAgentIdServicePrincipals -or $clientApps.excludeAgentIdServicePrincipals.Count -eq 0)) {
                                'All Agent Identities'
                            } else { 'Some Agent Identities or Blueprints' }
            $agentPolicies += [PSCustomObject]@{
                Policy       = $policy
                TargetedType = $targetedType
                IsBlocking   = ($agentIdentityBlockPolicies | Where-Object { $_.id -eq $policy.id }).Count -gt 0
            }
        }

        foreach ($policy in ($agentUserPolicies | Sort-Object displayName)) {
            $agents = $policy.conditions.agents
            $targetedType = if ($null -ne $agents -and
                                $agents.includeAgentUsers -contains 'All' -and
                                (@($agents.excludeAgentUsers).Count -eq 0) -and
                                $null -eq $agents.agentFilter) {
                                'All Agent Users'
                            } else { 'Some Agent Users' }
            $agentPolicies += [PSCustomObject]@{
                Policy       = $policy
                TargetedType = $targetedType
                IsBlocking   = ($agentUserBlockPolicies | Where-Object { $_.id -eq $policy.id }).Count -gt 0
            }
        }
    }

    $tableRows = ''
    if ($agentPolicies.Count -gt 0) {
        $rankedPolicies = @()
        foreach ($entry in $agentPolicies) {
            $pol = $entry.Policy
            $includedResources = @($pol.conditions.applications.includeApplications)
            $hasExceptions = (
                ($null -ne $pol.conditions.clientApplications.excludeAgentIdServicePrincipals -and @($pol.conditions.clientApplications.excludeAgentIdServicePrincipals).Count -gt 0) -or
                ($null -ne $pol.conditions.clientApplications.excludeServicePrincipals -and @($pol.conditions.clientApplications.excludeServicePrincipals).Count -gt 0) -or
                ($null -ne $pol.conditions.agents.excludeAgentUsers -and @($pol.conditions.agents.excludeAgentUsers).Count -gt 0) -or
                ($null -ne $pol.conditions.applications.excludeApplications -and @($pol.conditions.applications.excludeApplications).Count -gt 0) -or
                ($null -ne $pol.conditions.agents.agentFilter) -or
                ($null -ne $pol.conditions.applications.applicationFilter)
            )

            if ($pol.state -eq 'enabled' -and @($pol.grantControls.builtInControls) -contains 'block' -and $includedResources -contains 'All') {
                $status = if ($hasExceptions) { 'Pass (exceptions configured)' } else { 'Pass' }
                $priority = 2
            }
            elseif ($pol.state -eq 'enabled' -and @($pol.grantControls.builtInControls) -contains 'block' -and $includedResources -contains 'AllAgentIdResources') {
                $status = 'Investigate (CA does not cover all resources)'
                if ($hasExceptions) { $status += '; exceptions configured' }
                $priority = 1
            }
            else {
                $status = 'Fail'
                $priority = 0
            }

            $rankedPolicies += [PSCustomObject]@{
                Entry = $entry
                Status = $status
                Priority = $priority
            }
        }

        $rowPolicies = @($rankedPolicies |
            Sort-Object -Property Priority, @{ Expression = { $_.Entry.Policy.displayName } } |
            Select-Object -First 10)
        foreach ($entry in $rowPolicies) {
            $pol        = $entry.Entry.Policy
            $policyName = Get-SafeMarkdown -Text $pol.displayName
            $grants     = if ($pol.grantControls -and $pol.grantControls.builtInControls) { $pol.grantControls.builtInControls -join ', ' } else { '(none)' }
            $tableRows += "| [$policyName]($portalPolicyBaseUrl$($pol.id)) | $($entry.Entry.TargetedType) | $(Get-ZtCaPolicyState -State $pol.state) | $grants | $($entry.Status) |`n"
        }
        if ($agentPolicies.Count -gt 10) {
            $omittedCount = $agentPolicies.Count - $rowPolicies.Count
            $tableRows += "| ... | | | | $omittedCount more policies omitted |`n"
        }

        $mdInfo = $formatTemplate -f $reportTitle, $portalLink, $tableRows
    }

    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
    #endregion Report Generation

    $params = @{
        TestId = '61009'
        Title  = 'Conditional Access policies cover both agent identities and agent users'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }
    Add-ZtTestResultDetail @params
}