tests/Test-Assessment.41052.ps1

<#
.SYNOPSIS
    Application control is enforced via Intune.
 
.NOTES
    Test ID: 41052
    Required permission: DeviceManagementConfiguration.Read.All
#>


function Test-Assessment-41052 {
    [ZtTest(
        Category = 'Endpoint threat protection',
        CompatibleLicense = ('INTUNE_A'),
        ImplementationCost = 'High',
        Pillar = 'SecOps',
        RiskLevel = 'High',
        Service = ('Graph'),
        SfiPillar = 'Monitor and detect cyberthreats',
        TenantType = ('Workforce'),
        TestId = 41052,
        Title = 'Application control (WDAC / App Control for Business) is enforced via Intune',
        UserImpact = 'Medium'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection

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

    $activity = 'Checking App Control for Business policies'
    $queryError = $null
    $policies = @()

    Write-ZtProgress -Activity $activity -Status 'Getting Intune application control policies'
    try {
        $policies = @(Invoke-ZtGraphRequest -RelativeUri 'deviceManagement/configurationPolicies' -ApiVersion beta -ErrorAction Stop |
            Where-Object { $_.templateReference.templateFamily -eq 'endpointSecurityApplicationControl' })
    }
    catch {
        $queryError = $_
        Write-PSFMessage "Failed to retrieve App Control for Business policies: $_" -Tag Test -Level Warning
    }

    if ($queryError) {
        $statusCode = Get-ZtHttpStatusCode -ErrorRecord $queryError
        if ($statusCode -in 401, 403, 404) {
            Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'Microsoft Graph returned HTTP 401, 403, or 404. Verify Intune licensing, user RBAC, and DeviceManagementConfiguration.Read.All consent.'
            return
        }

        $params = @{
            TestId       = '41052'
            Title        = 'Application control (WDAC / App Control for Business) is enforced via Intune'
            Status       = $false
            Result       = '⚠️ Intune App Control for Business policies could not be retrieved. Retry after resolving the Microsoft Graph error.'
            CustomStatus = 'Investigate'
        }
        Add-ZtTestResultDetail @params
        return
    }

    $policyResults = @()
    foreach ($policy in $policies) {
        try {
            Write-ZtProgress -Activity $activity -Status "Reading policy $($policy.name)"
            $assignments = @(Invoke-ZtGraphRequest -RelativeUri "deviceManagement/configurationPolicies('$($policy.id)')/assignments" -ApiVersion beta -ErrorAction Stop)
        }
        catch {
            $statusCode = Get-ZtHttpStatusCode -ErrorRecord $_
            if ($statusCode -in 401, 403, 404) {
                Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'Microsoft Graph returned HTTP 401, 403, or 404. Verify Intune licensing, user RBAC, and DeviceManagementConfiguration.Read.All consent.'
                return
            }

            Write-PSFMessage "Failed to retrieve assignments for App Control policy '$($policy.name)': $_" -Tag Test -Level Warning
            $policyResults += [pscustomobject]@{
                Name = $policy.name; TemplateFamily = $policy.templateReference.templateFamily; Mode = 'Unknown'
                PolicyId = $policy.id; Technologies = $policy.technologies; TemplateId = $policy.templateReference.templateId; Platforms = $policy.platforms
                AssignmentState = 'Unknown'; AssignmentCount = $null; LastModified = $policy.lastModifiedDateTime; Status = 'Investigate'
            }
            continue
        }

        try {
            $settings = @(Invoke-ZtGraphRequest -RelativeUri "deviceManagement/configurationPolicies('$($policy.id)')/settings" -ApiVersion beta -ErrorAction Stop)
        }
        catch {
            $statusCode = Get-ZtHttpStatusCode -ErrorRecord $_
            if ($statusCode -in 401, 403, 404) {
                Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'Microsoft Graph returned HTTP 401, 403, or 404. Verify Intune licensing, user RBAC, and DeviceManagementConfiguration.Read.All consent.'
                return
            }

            Write-PSFMessage "Failed to retrieve settings for App Control policy '$($policy.name)': $_" -Tag Test -Level Warning
            $assignmentCount = $assignments.Count
            $policyResults += [pscustomobject]@{
                Name = $policy.name; TemplateFamily = $policy.templateReference.templateFamily; Mode = 'Unknown'
                PolicyId = $policy.id; Technologies = $policy.technologies; TemplateId = $policy.templateReference.templateId; Platforms = $policy.platforms
                AssignmentState = if ($assignmentCount -gt 0) { 'Assigned' } else { 'Unassigned' }; AssignmentCount = $assignmentCount; LastModified = $policy.lastModifiedDateTime
                Status = if ($assignmentCount -gt 0) { 'Investigate' } else { 'Fail' }
            }
            continue
        }

        $getPolicyMode = {
            param($PolicySettings)

            $buildOptions = @($PolicySettings | Where-Object {
                $_.settingInstance.settingDefinitionId -match '(?i)device_vendor_msft_policy_config_applicationcontrol' -and
                $_.settingInstance.choiceSettingValue.value -match '(?i)_(built_in_controls|configure_xml|upload_xml)_selected$'
            })
            if ($buildOptions.Count -ne 1) {
                return 'Unknown'
            }

            $buildOption = $buildOptions[0].settingInstance
            $buildOptionValue = [string]$buildOption.choiceSettingValue.value
            $instances = [System.Collections.Queue]::new()
            foreach ($child in @($buildOption.choiceSettingValue.children)) {
                if ($null -ne $child) {
                    $instances.Enqueue($child)
                }
            }

            if ($buildOptionValue -match '(?i)_built_in_controls_selected$') {
                $observedModes = @()
                while ($instances.Count -gt 0) {
                    $instance = $instances.Dequeue()
                    $settingId = [string]$instance.settingDefinitionId
                    $choiceValue = [string]$instance.choiceSettingValue.value
                    if ($settingId -eq 'device_vendor_msft_policy_config_applicationcontrol_enable_app_control') {
                        if ($choiceValue -eq 'device_vendor_msft_policy_config_applicationcontrol_enable_app_control_audit_only') {
                            $observedModes += 'Audit'
                        }
                        elseif ($choiceValue -eq 'device_vendor_msft_policy_config_applicationcontrol_enable_app_control_enabled') {
                            $observedModes += 'Enforce'
                        }
                    }

                    foreach ($child in @($instance.choiceSettingValue.children)) {
                        if ($null -ne $child) {
                            $instances.Enqueue($child)
                        }
                    }
                }

                $observedModes = @($observedModes | Select-Object -Unique)
                if ($observedModes.Count -eq 1) {
                    return $observedModes[0]
                }
                return 'Unknown'
            }

            $xmlModes = @()
            while ($instances.Count -gt 0) {
                $instance = $instances.Dequeue()
                $settingId = [string]$instance.settingDefinitionId
                $xmlValue = if ($settingId -match '(?i)applicationcontrol') {
                    [string]$instance.simpleSettingValue.value
                }

                if (-not [string]::IsNullOrWhiteSpace($xmlValue)) {
                    $xmlCandidates = @($xmlValue)
                    try {
                        $bytes = [Convert]::FromBase64String($xmlValue)
                        $xmlCandidates += [Text.Encoding]::UTF8.GetString($bytes)
                        $xmlCandidates += [Text.Encoding]::Unicode.GetString($bytes)
                        $xmlCandidates += [Text.Encoding]::BigEndianUnicode.GetString($bytes)
                    }
                    catch {
                        Write-PSFMessage "App Control policy XML value could not be decoded as Base64: $_" -Tag Test -Level VeryVerbose
                    }

                    foreach ($xmlCandidate in $xmlCandidates) {
                        try {
                            [xml]$policyXml = $xmlCandidate
                            if ($policyXml.DocumentElement.LocalName -ne 'SiPolicy') {
                                continue
                            }

                            $policyTypeNode = $policyXml.SelectSingleNode("//*[local-name()='PolicyType']")
                            if ($null -eq $policyTypeNode -or $policyTypeNode.InnerText.Trim() -ne 'Base Policy') {
                                continue
                            }

                            $auditOption = @($policyXml.SelectNodes("//*[local-name()='Option']") | Where-Object {
                                $_.InnerText.Trim() -eq 'Enabled:Audit Mode'
                            }).Count -gt 0
                            $xmlModes += if ($auditOption) { 'Audit' } else { 'Enforce' }
                            break
                        }
                        catch {
                            Write-PSFMessage "App Control policy XML value could not be parsed: $_" -Tag Test -Level VeryVerbose
                        }
                    }
                }

                foreach ($child in @($instance.choiceSettingValue.children)) {
                    if ($null -ne $child) {
                        $instances.Enqueue($child)
                    }
                }
            }

            $xmlModes = @($xmlModes | Select-Object -Unique)
            if ($xmlModes.Count -eq 1) {
                return $xmlModes[0]
            }
            return 'Unknown'
        }

        $mode = & $getPolicyMode $settings
        $assignmentCount = $assignments.Count
        $status = if ($assignmentCount -eq 0) { 'Fail' } elseif ($mode -eq 'Enforce') { 'Pass' } elseif ($mode -eq 'Unknown') { 'Investigate' } else { 'Fail' }

        $policyResults += [pscustomobject]@{
            Name = $policy.name; TemplateFamily = $policy.templateReference.templateFamily; Mode = $mode
            PolicyId = $policy.id; Technologies = $policy.technologies; TemplateId = $policy.templateReference.templateId; Platforms = $policy.platforms
            AssignmentState = if ($assignmentCount -gt 0) { 'Assigned' } else { 'Unassigned' }; AssignmentCount = $assignmentCount; LastModified = $policy.lastModifiedDateTime; Status = $status
        }
    }

    #endregion Data Collection

    #region Assessment Logic

    $enforcedPolicies = @($policyResults | Where-Object { $_.Status -eq 'Pass' })
    $unknownPolicies = @($policyResults | Where-Object { $_.Status -eq 'Investigate' })
    $passed = $enforcedPolicies.Count -gt 0
    $customStatus = $null

    if ($passed) {
        $testResultMarkdown = "✅ An App Control for Business policy is configured, enforced, and assigned in Microsoft Intune.`n`n%TestResult%"
    }
    elseif ($unknownPolicies.Count -gt 0) {
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ An App Control for Business policy was returned, but its assignment or enforcement mode could not be determined from the returned data.`n`n%TestResult%"
    }
    else {
        $testResultMarkdown = "❌ No App Control for Business policies were returned, no policies are assigned, or all assigned policies are audit-only.`n`n%TestResult%"
    }

    #endregion Assessment Logic

    #region Report Generation

    $portalUrl = 'https://intune.microsoft.com/#view/Microsoft_Intune_Workflows/SecurityManagementMenu/~/appControl'
    $tableRows = @($policyResults | Select-Object -First 10 | ForEach-Object {
        $lastModified = if ($_.LastModified) { Get-FormattedDate -DateString $_.LastModified } else { '—' }
        $policyName = (Get-SafeMarkdown $_.Name) -replace '\|', '\\|'
        $encodedTechnologies = ([string]$_.Technologies) -replace ',', '%2C'
        $policyLink = "https://intune.microsoft.com/#view/Microsoft_Intune_Workflows/PolicySummaryBlade/policyId/$($_.PolicyId)/technology/$encodedTechnologies/templateId/$($_.TemplateId)/platformName/$($_.Platforms)"
        "| [$policyName]($policyLink) | $($_.TemplateFamily) | $($_.Mode) | $($_.AssignmentState) | $($_.AssignmentCount) | $lastModified | $($_.Status) |"
    })
    if ($policyResults.Count -gt 10) {
        $tableRows += "| ... | | | | | | $($policyResults.Count) total policies |"
    }
    if ($tableRows.Count -eq 0) {
        $tableRows = '| No App Control for Business policies found | endpointSecurityApplicationControl | — | — | 0 | — | Fail |'
    }

    $mdInfo = @"
 
## [Intune App Control for Business]($portalUrl)
 
| Policy Name | Template Family | Mode | Assignment State | Assignment Count | Last Modified | Status |
| :---------- | :-------------- | :--- | :--------------- | ---------------: | :------------ | :----- |
$($tableRows -join "`n")
"@

    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo

    $params = @{
        TestId = '41052'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($null -ne $customStatus) {
        $params.CustomStatus = $customStatus
    }
    Add-ZtTestResultDetail @params

    #endregion Report Generation
}