Checks/Section07-Networking.ps1

# =============================================================================
# Section 7: Networking Services - Custom Check Functions
# CIS Microsoft Azure Foundations Benchmark v5.0.0
# =============================================================================
# Custom functions for NSG flow logs, Network Watcher, Application Gateway,
# WAF, and subnet checks. Dispatched via 'Custom' CheckPattern.
# Each function receives -ControlDef (hashtable) and -ResourceCache (hashtable).
# =============================================================================

function Test-FlowLogRetention {
    <#
    .SYNOPSIS
        Shared helper for NSG and VNet flow log retention checks.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache,

        [Parameter(Mandatory)]
        [string]$FlowLogType,

        [Parameter()]
        [string]$TargetResourceFilter = ''
    )

    try {
        $networkWatchers = @($ResourceCache.NetworkWatchers)
        if ($networkWatchers.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'WARNING' `
                -Details "No Network Watchers found. Cannot evaluate $FlowLogType flow log retention." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        $totalCount    = 0
        $failedList    = [System.Collections.Generic.List[string]]::new()
        $passedCount   = 0
        $errorWatchers = [System.Collections.Generic.List[string]]::new()

        foreach ($nw in $networkWatchers) {
            try {
                $flowLogs = @(Get-AzNetworkWatcherFlowLog -NetworkWatcherName $nw.Name -ResourceGroupName $nw.ResourceGroupName -ErrorAction Stop)

                # Filter by target resource type if specified
                if ($TargetResourceFilter) {
                    $flowLogs = @($flowLogs | Where-Object {
                        $_.TargetResourceId -match $TargetResourceFilter
                    })
                }

                foreach ($flowLog in $flowLogs) {
                    $totalCount++
                    $retentionEnabled = $flowLog.RetentionPolicy.Enabled
                    $retentionDays    = $flowLog.RetentionPolicy.Days

                    # Retention of 0 with enabled = true means indefinite (pass)
                    $minRetention = if ($script:CISConfig.RetentionThresholdDays) { $script:CISConfig.RetentionThresholdDays } else { 90 }
                    if ($retentionEnabled -and ($retentionDays -ge $minRetention -or $retentionDays -eq 0)) {
                        $passedCount++
                    }
                    else {
                        $currentRetention = if ($retentionEnabled) { "$retentionDays days" } else { 'disabled' }
                        $targetId = if ($flowLog.TargetResourceId) {
                            ($flowLog.TargetResourceId -split '/')[-1]
                        } else { $flowLog.Name }
                        $failedList.Add("$targetId (retention: $currentRetention)")
                    }
                }
            }
            catch {
                $errorWatchers.Add("$($nw.Name): $(Format-CISErrorMessage $_.Exception.Message)")
            }
        }

        # If ALL watchers failed to respond, return WARNING instead of false FAIL
        if ($totalCount -eq 0 -and $errorWatchers.Count -gt 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'WARNING' `
                -Details "Could not retrieve $FlowLogType flow logs from $($errorWatchers.Count) Network Watcher(s). Errors: $($errorWatchers -join '; ')" `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        if ($totalCount -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details "No $FlowLogType flow logs found across $($networkWatchers.Count) Network Watcher(s). Flow logs should be configured." `
                -AffectedResources @("No $FlowLogType flow logs configured") `
                -TotalResources 0 -PassedResources 0 -FailedResources 1
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $retentionDaysDisplay = if ($script:CISConfig.RetentionThresholdDays) { $script:CISConfig.RetentionThresholdDays } else { 90 }
            $details = "Found $failedCount of $totalCount $FlowLogType flow log(s) with retention < $retentionDaysDisplay days: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount $FlowLogType flow log(s) have retention >= $retentionDaysDisplay days." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking $FlowLogType flow log retention: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS75-NSGFlowLogRetention {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [hashtable]$ControlDef,
        [Parameter(Mandatory)] [hashtable]$ResourceCache
    )
    Test-FlowLogRetention -ControlDef $ControlDef -ResourceCache $ResourceCache -FlowLogType 'NSG' -TargetResourceFilter 'Microsoft\.Network/networkSecurityGroups'
}

function Test-CIS76-NetworkWatcher {
    <#
    .SYNOPSIS
        CIS 7.6 - Ensure Network Watcher is 'Enabled' for Azure Regions in use.
    .DESCRIPTION
        Identifies all regions where resources are deployed and checks that a
        Network Watcher instance exists in each region.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $networkWatchers = @($ResourceCache.NetworkWatchers)

        # Determine regions in use from VNets, NSGs, and other cached resources
        $usedRegions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

        $vnets = @($ResourceCache.VirtualNetworks)
        foreach ($vnet in $vnets) {
            if ($vnet.Location) { [void]$usedRegions.Add($vnet.Location) }
        }

        $nsgs = @($ResourceCache.NSGs)
        foreach ($nsg in $nsgs) {
            if ($nsg.Location) { [void]$usedRegions.Add($nsg.Location) }
        }

        $appGws = @($ResourceCache.ApplicationGateways)
        foreach ($appGw in $appGws) {
            if ($appGw.Location) { [void]$usedRegions.Add($appGw.Location) }
        }

        if ($usedRegions.Count -eq 0) {
            # Fallback: use locations from additional cached resources or Get-AzLocation
            $storageAccounts = @($ResourceCache.StorageAccounts)
            foreach ($sa in $storageAccounts) {
                if ($sa.Location) { [void]$usedRegions.Add($sa.Location) }
            }
            $keyVaults = @($ResourceCache.KeyVaults)
            foreach ($kv in $keyVaults) {
                if ($kv.Location) { [void]$usedRegions.Add($kv.Location) }
            }
            # No resources found in any cache — cannot determine which regions are in use.
            # Do NOT fall back to Get-AzLocation as that returns ALL Azure regions,
            # which would produce false failures for regions where no resources exist.
        }

        if ($usedRegions.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'WARNING' `
                -Details "Could not determine regions in use. Unable to validate Network Watcher coverage." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        $watcherRegions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($nw in $networkWatchers) {
            if ($nw.Location) { [void]$watcherRegions.Add($nw.Location) }
        }

        $totalCount  = $usedRegions.Count
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($region in $usedRegions) {
            if ($watcherRegions.Contains($region)) {
                $passedCount++
            }
            else {
                $failedList.Add($region)
            }
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "Network Watcher is missing in $failedCount of $totalCount region(s): $($failedList -join ', ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "Network Watcher is enabled in all $totalCount region(s) in use: $($usedRegions -join ', ')." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking Network Watcher coverage: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS78-VNetFlowLogRetention {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [hashtable]$ControlDef,
        [Parameter(Mandatory)] [hashtable]$ResourceCache
    )
    Test-FlowLogRetention -ControlDef $ControlDef -ResourceCache $ResourceCache -FlowLogType 'VNet' -TargetResourceFilter 'Microsoft\.Network/virtualNetworks'
}

function Test-CIS710-AppGatewayWAF {
    <#
    .SYNOPSIS
        CIS 7.10 - Ensure Azure WAF is enabled on Application Gateway.
    .DESCRIPTION
        Checks that Application Gateways have WAF enabled via WAF_v2 SKU with
        WebApplicationFirewallConfiguration or an associated WAF policy.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $appGateways = @($ResourceCache.ApplicationGateways)
        if ($appGateways.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No Application Gateways found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        $totalCount  = $appGateways.Count
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($gw in $appGateways) {
            $hasWAF = $false

            # Check SKU for WAF tier
            if ($gw.Sku -and $gw.Sku.Tier -match 'WAF') {
                $hasWAF = $true
            }

            # Check for WebApplicationFirewallConfiguration
            if (-not $hasWAF -and $gw.WebApplicationFirewallConfiguration -and $gw.WebApplicationFirewallConfiguration.Enabled) {
                $hasWAF = $true
            }

            # Check for FirewallPolicy (WAF policy attached)
            if (-not $hasWAF -and $gw.FirewallPolicy -and $gw.FirewallPolicy.Id) {
                $hasWAF = $true
            }

            if ($hasWAF) {
                $passedCount++
            }
            else {
                $skuTier = if ($gw.Sku) { $gw.Sku.Tier } else { 'Unknown' }
                $failedList.Add("$($gw.Name) (SKU: $skuTier)")
            }
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "Found $failedCount of $totalCount Application Gateway(s) without WAF enabled: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount Application Gateway(s) have WAF enabled." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking Application Gateway WAF: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS711-SubnetNSG {
    <#
    .SYNOPSIS
        CIS 7.11 - Ensure subnets are associated with network security groups.
    .DESCRIPTION
        Checks all subnets across all VNets, excluding special subnets like
        GatewaySubnet, AzureBastionSubnet, AzureFirewallSubnet, and
        AzureFirewallManagementSubnet.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $vnets = @($ResourceCache.VirtualNetworks)
        if ($vnets.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No Virtual Networks found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        # Subnets that do not require or support NSGs
        $exemptSubnets = @(
            'GatewaySubnet',
            'AzureBastionSubnet',
            'AzureFirewallSubnet',
            'AzureFirewallManagementSubnet',
            'RouteServerSubnet'
        )

        $totalCount  = 0
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($vnet in $vnets) {
            foreach ($subnet in $vnet.Subnets) {
                if ($subnet.Name -in $exemptSubnets) { continue }

                $totalCount++
                if ($subnet.NetworkSecurityGroup -and $subnet.NetworkSecurityGroup.Id) {
                    $passedCount++
                }
                else {
                    $failedList.Add("$($vnet.Name)/$($subnet.Name)")
                }
            }
        }

        if ($totalCount -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No evaluable subnets found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "Found $failedCount of $totalCount subnet(s) without NSGs: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount evaluable subnet(s) have NSGs associated." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking subnet NSG associations: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS712-AppGatewayTLS {
    <#
    .SYNOPSIS
        CIS 7.12 - Ensure SSL policy MinProtocolVersion is TLSv1_2 or higher.
    .DESCRIPTION
        Checks each Application Gateway's SSL policy for minimum protocol version
        of TLSv1_2 or TLSv1_3.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $appGateways = @($ResourceCache.ApplicationGateways)
        if ($appGateways.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No Application Gateways found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        $acceptableVersions = @('TLSv1_2', 'TLSv1_3')
        $totalCount  = $appGateways.Count
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($gw in $appGateways) {
            $minVersion = $null

            if ($gw.SslPolicy) {
                $minVersion = $gw.SslPolicy.MinProtocolVersion
                # Also check predefined policy names that enforce TLS 1.2+
                if (-not $minVersion -and $gw.SslPolicy.PolicyName) {
                    $policyName = $gw.SslPolicy.PolicyName
                    # Predefined policies with TLS 1.2 minimum
                    $tls12Policies = @(
                        'AppGwSslPolicy20170401S',
                        'AppGwSslPolicy20220101',
                        'AppGwSslPolicy20220101S',
                        'AppGwSslPolicy20230202',
                        'AppGwSslPolicy20230202S'
                    )
                    if ($policyName -in $tls12Policies) {
                        $minVersion = 'TLSv1_2'
                    }
                }
            }

            if ($minVersion -and $minVersion -in $acceptableVersions) {
                $passedCount++
            }
            else {
                $currentVersion = if ($minVersion) { $minVersion } else { 'Not set (defaults may allow TLS 1.0/1.1)' }
                $failedList.Add("$($gw.Name) (MinProtocolVersion: $currentVersion)")
            }
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "Found $failedCount of $totalCount Application Gateway(s) without TLS 1.2+ minimum: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount Application Gateway(s) enforce TLS 1.2 or higher." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking Application Gateway TLS policy: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS713-AppGatewayHTTP2 {
    <#
    .SYNOPSIS
        CIS 7.13 - Ensure 'HTTP2' is set to 'Enabled' on Azure Application Gateway.
    .DESCRIPTION
        Checks that EnableHttp2 is true on each Application Gateway.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $appGateways = @($ResourceCache.ApplicationGateways)
        if ($appGateways.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No Application Gateways found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        $totalCount  = $appGateways.Count
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($gw in $appGateways) {
            if ($gw.EnableHttp2 -eq $true) {
                $passedCount++
            }
            else {
                $failedList.Add("$($gw.Name) (EnableHttp2: $($gw.EnableHttp2))")
            }
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "Found $failedCount of $totalCount Application Gateway(s) without HTTP/2 enabled: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount Application Gateway(s) have HTTP/2 enabled." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking Application Gateway HTTP/2: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS714-WAFRequestBodyInspection {
    <#
    .SYNOPSIS
        CIS 7.14 - Ensure request body inspection is enabled in WAF policy.
    .DESCRIPTION
        Checks WAF policies associated with Application Gateways to verify
        request body inspection is enabled.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $appGateways = @($ResourceCache.ApplicationGateways)
        if ($appGateways.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No Application Gateways found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        # Collect unique WAF policy IDs from Application Gateways
        $wafPolicyIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($gw in $appGateways) {
            if ($gw.FirewallPolicy -and $gw.FirewallPolicy.Id) {
                [void]$wafPolicyIds.Add($gw.FirewallPolicy.Id)
            }
        }

        if ($wafPolicyIds.Count -eq 0) {
            # Check inline WAF configuration
            $gwsWithWAF = @($appGateways | Where-Object { $_.WebApplicationFirewallConfiguration })
            if ($gwsWithWAF.Count -eq 0) {
                return New-CISCheckResult `
                    -ControlId $ControlDef.ControlId `
                    -Title $ControlDef.Title `
                    -Status 'WARNING' `
                    -Details "No WAF policies or configurations found on $($appGateways.Count) Application Gateway(s)." `
                    -TotalResources $appGateways.Count -PassedResources 0 -FailedResources 0
            }

            $totalCount  = $gwsWithWAF.Count
            $failedList  = [System.Collections.Generic.List[string]]::new()
            $passedCount = 0

            foreach ($gw in $gwsWithWAF) {
                if ($gw.WebApplicationFirewallConfiguration.RequestBodyCheck -eq $true) {
                    $passedCount++
                }
                else {
                    $failedList.Add("$($gw.Name) (inline WAF: RequestBodyCheck disabled)")
                }
            }

            $failedCount = $failedList.Count
            if ($failedCount -gt 0) {
                return New-CISCheckResult `
                    -ControlId $ControlDef.ControlId `
                    -Title $ControlDef.Title `
                    -Status 'FAIL' `
                    -Details "$failedCount of $totalCount WAF configuration(s) have request body inspection disabled: $($failedList -join '; ')" `
                    -AffectedResources $failedList.ToArray() `
                    -TotalResources $totalCount -PassedResources $passedCount -FailedResources $failedCount
            }

            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "All $totalCount WAF configuration(s) have request body inspection enabled." `
                -TotalResources $totalCount -PassedResources $passedCount -FailedResources 0
        }

        # Check WAF policies
        $totalCount  = $wafPolicyIds.Count
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($policyId in $wafPolicyIds) {
            try {
                # Use cached WAF policy if available, otherwise fetch
                $policy = if ($ResourceCache.WafPolicies -and $ResourceCache.WafPolicies.ContainsKey($policyId)) {
                    $ResourceCache.WafPolicies[$policyId]
                } else {
                    Get-AzResource -ResourceId $policyId -ExpandProperties -ErrorAction Stop
                }
                $policyName = ($policyId -split '/')[-1]

                $requestBodyCheck = $false
                if ($policy.Properties.PolicySettings -and $policy.Properties.PolicySettings.RequestBodyCheck -eq $true) {
                    $requestBodyCheck = $true
                }
                # Alternative property path
                if (-not $requestBodyCheck -and $policy.Properties.policySettings -and $policy.Properties.policySettings.requestBodyCheck -eq $true) {
                    $requestBodyCheck = $true
                }

                if ($requestBodyCheck) {
                    $passedCount++
                }
                else {
                    $failedList.Add($policyName)
                }
            }
            catch {
                $policyName = ($policyId -split '/')[-1]
                $failedList.Add("$policyName [Error: $($_.Exception.Message)]")
            }
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "$failedCount of $totalCount WAF policy(ies) have request body inspection disabled: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount WAF policy(ies) have request body inspection enabled." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking WAF request body inspection: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}

function Test-CIS715-WAFBotProtection {
    <#
    .SYNOPSIS
        CIS 7.15 - Ensure bot protection is enabled in WAF policy.
    .DESCRIPTION
        Checks WAF policies for the presence of bot protection managed rule sets.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$ControlDef,

        [Parameter(Mandatory)]
        [hashtable]$ResourceCache
    )

    try {
        $appGateways = @($ResourceCache.ApplicationGateways)
        if ($appGateways.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'PASS' `
                -Details "N/A - No Application Gateways found in the subscription. Control not evaluated." `
                -TotalResources 0 -PassedResources 0 -FailedResources 0
        }

        # Collect unique WAF policy IDs
        $wafPolicyIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($gw in $appGateways) {
            if ($gw.FirewallPolicy -and $gw.FirewallPolicy.Id) {
                [void]$wafPolicyIds.Add($gw.FirewallPolicy.Id)
            }
        }

        if ($wafPolicyIds.Count -eq 0) {
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'WARNING' `
                -Details "No WAF policies found on $($appGateways.Count) Application Gateway(s). Bot protection requires WAF policy." `
                -TotalResources $appGateways.Count -PassedResources 0 -FailedResources 0
        }

        $totalCount  = $wafPolicyIds.Count
        $failedList  = [System.Collections.Generic.List[string]]::new()
        $passedCount = 0

        foreach ($policyId in $wafPolicyIds) {
            try {
                # Use cached WAF policy if available, otherwise fetch
                $policy = if ($ResourceCache.WafPolicies -and $ResourceCache.WafPolicies.ContainsKey($policyId)) {
                    $ResourceCache.WafPolicies[$policyId]
                } else {
                    Get-AzResource -ResourceId $policyId -ExpandProperties -ErrorAction Stop
                }
                $policyName = ($policyId -split '/')[-1]

                $hasBotProtection = $false

                # Check ManagedRules for bot protection rule set
                $managedRules = $policy.Properties.ManagedRules
                if (-not $managedRules) { $managedRules = $policy.Properties.managedRules }

                if ($managedRules -and $managedRules.ManagedRuleSets) {
                    foreach ($ruleSet in $managedRules.ManagedRuleSets) {
                        if ($ruleSet.RuleSetType -match 'BotProtection|Microsoft_BotManagerRuleSet') {
                            $hasBotProtection = $true
                            break
                        }
                    }
                }
                if (-not $hasBotProtection -and $managedRules -and $managedRules.managedRuleSets) {
                    foreach ($ruleSet in $managedRules.managedRuleSets) {
                        $ruleSetType = if ($ruleSet.ruleSetType) { $ruleSet.ruleSetType } else { $ruleSet.RuleSetType }
                        if ($ruleSetType -match 'BotProtection|Microsoft_BotManagerRuleSet') {
                            $hasBotProtection = $true
                            break
                        }
                    }
                }

                if ($hasBotProtection) {
                    $passedCount++
                }
                else {
                    $failedList.Add($policyName)
                }
            }
            catch {
                $policyName = ($policyId -split '/')[-1]
                $failedList.Add("$policyName [Error: $($_.Exception.Message)]")
            }
        }

        $failedCount = $failedList.Count
        if ($failedCount -gt 0) {
            $details = "$failedCount of $totalCount WAF policy(ies) do not have bot protection enabled: $($failedList -join '; ')"
            return New-CISCheckResult `
                -ControlId $ControlDef.ControlId `
                -Title $ControlDef.Title `
                -Status 'FAIL' `
                -Details $details `
                -AffectedResources $failedList.ToArray() `
                -TotalResources $totalCount `
                -PassedResources $passedCount `
                -FailedResources $failedCount
        }

        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'PASS' `
            -Details "All $totalCount WAF policy(ies) have bot protection enabled." `
            -TotalResources $totalCount `
            -PassedResources $passedCount `
            -FailedResources 0
    }
    catch {
        return New-CISCheckResult `
            -ControlId $ControlDef.ControlId `
            -Title $ControlDef.Title `
            -Status 'ERROR' `
            -Details "Error checking WAF bot protection: $(Format-CISErrorMessage $_.Exception.Message)"
    }
}