tests/Test-Assessment.25401.ps1

<#
.SYNOPSIS
    Validates that Application Proxy applications require pre-authentication to block anonymous access.
 
.DESCRIPTION
    This test checks whether Application Proxy applications are configured with Microsoft Entra
    pre-authentication instead of passthrough authentication. Passthrough authentication allows
    unauthenticated access to on-premises resources, exposing them to potential threat actors.
 
    The test verifies:
    - All Application Proxy applications use Microsoft Entra ID pre-authentication
    - No applications are configured with passthrough authentication
 
.NOTES
    Test ID: 25401
    Category: Application Proxy
    Pillar: Network
    Required API: applications (beta)
    Note: Two-query approach required due to bulk API returning default values
#>


function Test-Assessment-25401 {
    [ZtTest(
        Category = 'Application Proxy',
        ImplementationCost = 'Medium',
        MinimumLicense = ('AAD_PREMIUM'),
        Pillar = 'Network',
        RiskLevel = 'High',
        SfiPillar = 'Protect identities and secrets',
        TenantType = ('Workforce'),
        TestId = 25401,
        Title = 'Application Proxy applications require pre-authentication to block anonymous access to on-premises resources',
        UserImpact = 'Medium'
    )]
    [CmdletBinding()]
    param(
        $Database
    )

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

    $activity = 'Checking Application Proxy pre-authentication configuration'
    Write-ZtProgress -Activity $activity -Status 'Querying Application Proxy applications'

    $appProxyAppsFailed = $false
    $appProxyApps = @()

    # Query 1: Retrieve the list of Application Proxy-enabled applications
    try {
        $appProxyApps = Invoke-ZtGraphRequest `
            -RelativeUri 'applications' `
            -Filter 'onPremisesPublishing/isOnPremPublishingEnabled eq true' `
            -Select 'id','displayName' `
            -ApiVersion beta
    }
    catch {
        $appProxyAppsFailed = $true
        Write-PSFMessage "Failed to retrieve Application Proxy applications: $_" -Tag Test -Level Warning
    }

    # Query 2: Collect detailed configuration for all applications
    $appDetailsCollection = @()
    if (-not $appProxyAppsFailed -and $appProxyApps.Count -gt 0) {
        Write-ZtProgress -Activity $activity -Status 'Retrieving application details'

        foreach ($app in $appProxyApps) {
            try {
                $appDetail = Invoke-ZtGraphRequest `
                    -RelativeUri "applications/$($app.id)" `
                    -Select 'id','appId','displayName','onPremisesPublishing' `
                    -ApiVersion beta

                if ($null -eq $appDetail -or $null -eq $appDetail.onPremisesPublishing) {
                    continue
                }

                $appDetailsCollection += $appDetail
            }
            catch {
                Write-PSFMessage "Failed to retrieve details for application $($app.id): $_" -Tag Test -Level Warning
            }
        }
    }

    # Lookup service principal IDs from database for building deep links
    $spIdLookup = @{}
    if ($appDetailsCollection.Count -gt 0) {
        try {
            # Collect unique appIds
            $appIds = $appDetailsCollection | Where-Object { $_.appId } | Select-Object -ExpandProperty appId -Unique

            if ($appIds.Count -gt 0) {
                # Build IN clause for all appIds
                $appIdInClause = ($appIds | ForEach-Object { "'$($_.Replace("'", "''"))'" }) -join ','

                # Single query to get all service principal IDs
                $spQuery = "SELECT id, appId FROM ServicePrincipal WHERE appId IN ($appIdInClause)"
                $spResults = @(Invoke-DatabaseQuery -Database $Database -Sql $spQuery -AsCustomObject)

                # Build lookup hashtable: appId -> service principal id
                foreach ($sp in $spResults) {
                    if ($sp.appId -and $sp.id) {
                        $spIdLookup["$($sp.appId)"] = $sp.id
                    }
                }
            }
        }
        catch {
            Write-PSFMessage "Failed to retrieve service principal IDs from database: $_" -Tag Test -Level Warning
        }
    }
    #endregion Data Collection

    #region Assessment Logic
    $testResultMarkdown = ''
    $passed = $false
    $customStatus = $null
    $allApplications = [System.Collections.Generic.List[object]]::new()

    # Check if query failed
    if ($appProxyAppsFailed) {
        Write-PSFMessage 'Failed to query Application Proxy applications due to API/permission error.' -Tag Test -Level Warning
        $testResultMarkdown = "⚠️ Unable to determine Application Proxy pre-authentication configuration due to query failure, connection issues, or insufficient permissions.`n`n%TestResult%"
        $passed = $false
        $customStatus = 'Investigate'
    }
    # No Application Proxy applications found
    elseif ($null -eq $appProxyApps -or $appProxyApps.Count -eq 0) {
        Write-PSFMessage 'No Application Proxy applications found in this tenant.' -Tag Test -Level Verbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'No Application Proxy applications are configured in this tenant.'
        return
    }
    else {
        Write-ZtProgress -Activity $activity -Status 'Analyzing pre-authentication settings'

        # Process application details and build final list
        foreach ($appDetail in $appDetailsCollection) {
            $authType = $appDetail.onPremisesPublishing.externalAuthenticationType
            $isCompliant = $authType -eq 'aadPreAuthentication'

            # Lookup service principal ID from hashtable
            $servicePrincipalId = $null
            if ($appDetail.appId -and $spIdLookup.ContainsKey("$($appDetail.appId)")) {
                $servicePrincipalId = $spIdLookup["$($appDetail.appId)"]
            }

            $appInfo = [PSCustomObject]@{
                Id                       = $appDetail.id
                AppId                    = $appDetail.appId
                ServicePrincipalId       = $servicePrincipalId
                DisplayName              = $appDetail.displayName
                ExternalAuthenticationType = $authType
                IsCompliant              = $isCompliant
                ComplianceStatus         = if ($isCompliant) { '✅ Yes' } else { '❌ No' }
            }

            $allApplications.Add($appInfo)
        }

        # Guard: If we couldn't retrieve details for any of the applications, treat as query failure
        if ($allApplications.Count -eq 0 -and $appProxyApps.Count -gt 0) {
            Write-PSFMessage 'Failed to retrieve details for any Application Proxy applications.' -Tag Test -Level Warning
            $testResultMarkdown = "⚠️ Unable to determine Application Proxy pre-authentication configuration due to query failure, connection issues, or insufficient permissions.`n`n%TestResult%"
            $passed = $false
            $customStatus = 'Investigate'
        }
        # Evaluate test result
        elseif ($allApplications.Count -gt 0) {
            $nonCompliantCount = ($allApplications | Where-Object { -not $_.IsCompliant }).Count

            if ($nonCompliantCount -eq 0) {
                # All applications use pre-authentication - pass
                $passed = $true
                $testResultMarkdown = "✅ All Application Proxy applications are configured with Microsoft Entra pre-authentication, ensuring users must authenticate before accessing on-premises resources.`n`n%TestResult%"
            }
            else {
                # One or more applications are not configured with Microsoft Entra pre-authentication - fail
                $passed = $false
                $testResultMarkdown = "❌ One or more Application Proxy applications are configured with passthrough authentication, allowing unauthenticated access to on-premises resources.`n`n%TestResult%"
            }
        }
    }
    #endregion Assessment Logic

    #region Report Generation
    $mdInfo = ''

    if ($allApplications.Count -gt 0) {
        $reportTitle = 'Application Proxy Pre-Authentication Configuration'

        $formatTemplate = @'
 
 
## {0}
 
| Application name | Pre-Authentication type | Compliant |
| :--------------- | :---------------------- | :-------- |
{1}
 
'@


        # Build table rows
        $tableRows = ''
        foreach ($app in $allApplications) {
            $appName = Get-SafeMarkdown -Text $app.DisplayName

            # Create deep link to Application Proxy page if we have service principal ID
            if ($app.ServicePrincipalId -and $app.AppId) {
                $appProxyUrl = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/AppProxy/objectId/$($app.ServicePrincipalId)/appId/$($app.AppId)/preferredSingleSignOnMode~/null/servicePrincipalType/Application/fromNav/"
                $appLink = "[$appName]($appProxyUrl)"
            }
            else {
                $appLink = $appName
            }

            $authType = Get-SafeMarkdown -Text $app.ExternalAuthenticationType
            $compliant = $app.ComplianceStatus

            $tableRows += "| $appLink | $authType | $compliant |`n"
        }

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

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

    $params = @{
        TestId = '25401'
        Title  = 'Application Proxy applications require pre-authentication to block anonymous access to on-premises resources'
        Status = $passed
        Result = $testResultMarkdown
    }

    if ($null -ne $customStatus) {
        $params.CustomStatus = $customStatus
    }

    Add-ZtTestResultDetail @params
}