functions/System/Hardening/Test-HardeningCompliance.ps1

function Test-HardeningCompliance {
    <#
    .SYNOPSIS
    Verifies that hardening rules have been successfully applied to the system.

    .DESCRIPTION
    Tests each hardening rule in a session against the actual system state to
    determine compliance. Compares applied rule configurations with current
    system values and generates a detailed compliance report.

    Supports multiple test modes:
    - Full: Verify all rules in a profile (default)
    - Delta: Verify only previously applied rules (after Invoke-SecurityHardening)
    - Custom: Verify specific rules by filter

    Returns a comprehensive compliance report with:
    - Per-rule verification results
    - Compliance percentage by category
    - System state snapshots
    - Remediation recommendations for non-compliant rules

    .PARAMETER Session
    The hardening session object from New-HardeningSession.
    Must have been passed to Invoke-SecurityHardening.
    Mandatory.

    .PARAMETER RuleFilter
    Optional array of specific rule names to verify.
    If omitted, verifies all rules in session profile.

    .PARAMETER Detailed
    If specified, includes full rule details and system values in report.
    Useful for debugging compliance issues.

    .PARAMETER Remediate
    If specified, automatically attempts to remediate non-compliant rules.
    Requires admin rights. Returns remediation results.
    Supports -WhatIf to preview remediation without making changes.

    .EXAMPLE
    $session = New-HardeningSession -Profile Recommended -TargetSystem Client -OSVersion 11 -SkipPrerequisiteCheck
    Invoke-SecurityHardening -Session $session
    $compliance = Test-HardeningCompliance -Session $session
    $compliance.CompliancePercentage

    Applies hardening and verifies compliance.

    .EXAMPLE
    $compliance = Test-HardeningCompliance -Session $session -Detailed
    $compliance.RuleResults | Where-Object { $_.Compliant -eq $false } | Select-Object RuleName, Expected, Actual

    Shows non-compliant rules with actual vs. expected values.

    .EXAMPLE
    $remediation = Test-HardeningCompliance -Session $session -Remediate
    $remediation.RemediatedRules | ForEach-Object { "$_.RuleName : $_ComplianceStatus" }

    Checks compliance and attempts to fix non-compliant rules.

    .NOTES
    DEPENDENCIES: Write-Log (Core), Get-HardeningProfile (System), Invoke-SecurityHardening (System)
    ERROR HANDLING: Logs all failures, continues testing other rules
    LOGGING: All verification results logged with timestamps
    ADMIN REQUIREMENT: Full verification and remediation require admin rights
    PERFORMANCE: Completes in <10 seconds for typical profiles
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSCustomObject]
        $Session,

        [Parameter(Mandatory = $false)]
        [string[]]
        $RuleFilter,

        [switch]
        $Detailed,

        [switch]
        $Remediate
    )

    begin {
        $ErrorActionPreference = 'Stop'
    }

    process {
        try {
            Write-Log -Message "Starting hardening compliance verification: Profile=$($Session.Profile), Mode=$(if($Remediate){'Remediate'}else{'Verify'})" -Level Info

            # Validate session
            if ($null -eq $Session.State) {
                throw "Invalid session object: missing State property"
            }

            # Load profile rules
            $hardeningProfile = Get-HardeningProfile -ProfileName $Session.Profile -TargetSystem $Session.TargetSystem

            # Filter rules if specified
            $rulesToTest = $hardeningProfile.Rules
            if ($PSBoundParameters.ContainsKey('RuleFilter')) {
                $rulesToTest = @($hardeningProfile.Rules | Where-Object { $_.Name -in $RuleFilter })
                Write-Log -Message "Testing $($rulesToTest.Count) filtered rules" -Level Info
            }

            $complianceResults = @()
            $compliantCount = 0
            $nonCompliantCount = 0
            $categoryStats = @{}

            # Test each rule
            foreach ($rule in $rulesToTest) {
                $ruleResult = _TestRuleCompliance -Rule $rule

                if ($ruleResult.Compliant) {
                    $compliantCount++
                }
                else {
                    $nonCompliantCount++

                    # Attempt remediation if requested
                    if ($Remediate) {
                        if ($PSCmdlet.ShouldProcess("Rule: $($rule.Name)", "Remediate non-compliant rule")) {
                            Write-Log -Message "Attempting to remediate rule: $($rule.Name)" -Level Warning
                            $remediationResult = _RemediateRule -Rule $rule
                            $ruleResult.RemediationAttempted = $true
                            $ruleResult.RemediationSuccess = $remediationResult
                        }
                    }
                }

                $complianceResults += $ruleResult

                # Track category statistics
                if (-not $categoryStats.ContainsKey($rule.Category)) {
                    $categoryStats[$rule.Category] = @{ Total = 0; Compliant = 0 }
                }
                $categoryStats[$rule.Category].Total++
                if ($ruleResult.Compliant) {
                    $categoryStats[$rule.Category].Compliant++
                }
            }

            # Calculate compliance metrics
            $totalRules = @($rulesToTest).Count
            $compliancePercentage = if ($totalRules -gt 0) {
                [math]::Round(($compliantCount / $totalRules) * 100, 2)
            }
            else {
                0
            }

            $categoryBreakdown = @{}
            foreach ($category in $categoryStats.Keys) {
                $stats = $categoryStats[$category]
                $categoryBreakdown[$category] = [ordered]@{
                    Total = $stats.Total
                    Compliant = $stats.Compliant
                    NonCompliant = $stats.Total - $stats.Compliant
                    Percentage = [math]::Round(($stats.Compliant / $stats.Total) * 100, 2)
                }
            }

            # Determine overall status
            $status = switch ($compliancePercentage) {
                100 {
                    'Fully Compliant'
                }
                { $_ -ge 95 } {
                    'Highly Compliant'
                }
                { $_ -ge 80 } {
                    'Mostly Compliant'
                }
                { $_ -ge 50 } {
                    'Partially Compliant'
                }
                default {
                    'Non-Compliant'
                }
            }

            Write-Log -Message "Compliance verification complete: $compliancePercentage% compliant ($compliantCount/$totalRules rules)" -Level Info

            # Build result object
            $result = [ordered]@{
                SessionId = $Session.SessionId
                Profile = $Session.Profile
                TargetSystem = $Session.TargetSystem
                VerificationTime = Get-Date
                CompliancePercentage = $compliancePercentage
                Status = $status
                TotalRules = $totalRules
                CompliantRules = $compliantCount
                NonCompliantRules = $nonCompliantCount
                CategoryBreakdown = $categoryBreakdown
                RuleResults = $complianceResults
                RemediationAttempted = $Remediate
                RemediatedRules = @($complianceResults | Where-Object { $_.RemediationSuccess -eq $true })
            }

            [PSCustomObject]$result
        }
        catch {
            Write-ErrorLog -Message "Failed to test hardening compliance: $($_.Exception.Message)" -Caller $MyInvocation.MyCommand.Name
            throw
        }
    }
}

# ================================================================================
# Private Helper Functions
# ================================================================================

function _TestRuleCompliance {
    <#
    .SYNOPSIS
    Tests a single rule for compliance against system state.
    #>

    [CmdletBinding()]
    param(
        [PSCustomObject]$Rule
    )

    $result = [ordered]@{
        RuleName = $Rule.Name
        Category = $Rule.Category
        Severity = $Rule.Severity
        Compliant = $false
        ExpectedValue = $null
        ActualValue = $null
        RemediationAttempted = $false
        RemediationSuccess = $false
    }

    try {
        # Skip verification if no verification data
        if ($null -eq $Rule.Verification) {
            $result.Compliant = $true
            $result.VerificationSkipped = $true
            return $result
        }

        $verification = $Rule.Verification

        # Execute verification command
        # NOTE: Invoke-Expression is SAFE here (ADR-004 exception, CLAUDE.md Regel 1.4):
        # Commands come from hardening profiles (.psd1 files), not from user input.
        # Profile data is static and loaded from trusted files only (not user-generated).
        # This is an approved exception for trusted, non-user-input code execution.
        if ($verification.ContainsKey('Command')) {
            # Profile data is trusted (loaded from .psd1 files), NOT user input
            $scriptBlock = [scriptblock]::Create($verification.Command)
            $actualValue = & $scriptBlock -ErrorAction SilentlyContinue
            $expectedValue = $verification.Expected

            $result.ActualValue = $actualValue
            $result.ExpectedValue = $expectedValue

            # Compare values - extract scalar values from registry objects
            if ($null -eq $actualValue) {
                $result.Compliant = $false
                Write-Log -Message "Rule not compliant: $($Rule.Name) - No value found" -Level Warning
            }
            else {
                # Extract value from Registry object if needed
                $compareActualValue = $actualValue
                if ($actualValue -is [System.Management.Automation.PSCustomObject]) {
                    # Try to extract scalar property values from PSCustomObject (registry results)
                    $properties = $actualValue.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' }
                    if ($properties.Count -eq 1) {
                        $compareActualValue = $properties[0].Value
                    }
                }

                # Handle different comparison types
                if ($compareActualValue -is [array] -and $expectedValue -is [array]) {
                    $result.Compliant = @(Compare-Object -ReferenceObject $expectedValue -DifferenceObject $compareActualValue).Count -eq 0
                }
                elseif ($compareActualValue -is [hashtable] -and $expectedValue -is [hashtable]) {
                    $result.Compliant = $true
                    foreach ($key in $expectedValue.Keys) {
                        if ($compareActualValue[$key] -ne $expectedValue[$key]) {
                            $result.Compliant = $false
                            break
                        }
                    }
                }
                else {
                    # For string comparisons, check if expected value is contained (for audit policy output)
                    if ($compareActualValue -is [string] -and $expectedValue -is [string]) {
                        $result.Compliant = $compareActualValue -match [regex]::Escape($expectedValue) -or $compareActualValue -like "*$expectedValue*"
                    }
                    else {
                        $result.Compliant = $compareActualValue -eq $expectedValue
                    }
                }

                if (-not $result.Compliant) {
                    Write-Log -Message "Rule not compliant: $($Rule.Name) - Expected: $expectedValue, Got: $compareActualValue" -Level Warning
                }
            }
        }
        elseif ($verification.ContainsKey('Type') -and $verification.Type -eq 'RegistryMultiple') {
            # Structured verification for multiple registry paths (no string-based code eval)
            $actualValues = @()
            foreach ($path in $verification.Paths) {
                try {
                    $value = Get-ItemProperty -Path $path -Name $verification.PropertyName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $verification.PropertyName
                    $actualValues += $value
                }
                catch {
                    $actualValues += $null
                }
            }

            $expectedValues = $verification.ExpectedValues
            $result.ActualValue = $actualValues
            $result.ExpectedValue = $expectedValues

            if ($null -eq $actualValues -or $actualValues.Count -ne $expectedValues.Count) {
                $result.Compliant = $false
            }
            else {
                $result.Compliant = @(Compare-Object -ReferenceObject $expectedValues -DifferenceObject $actualValues).Count -eq 0
            }

            if (-not $result.Compliant) {
                Write-Log -Message "Rule not compliant: $($Rule.Name) - Expected: @($($expectedValues -join ', ')), Got: @($($actualValues -join ', '))" -Level Warning
            }
        }

        $result
    }
    catch {
        Write-Log -Message "Error testing rule $($Rule.Name): $($_.Exception.Message)" -Level Warning
        $result.Compliant = $false
        $result.VerificationError = $_.Exception.Message
        $result
    }
}

function _RemediateRule {
    <#
    .SYNOPSIS
    Attempts to remediate a single non-compliant rule.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [PSCustomObject]$Rule
    )

    try {
        # Re-apply the rule by calling the appropriate handler
        switch ($Rule.Type) {
            'Registry' {
                _ApplyRegistryRule -Rule $Rule
                $true
            }
            'Service' {
                _ApplyServiceRule -Rule $Rule
                $true
            }
            'Firewall' {
                _ApplyFirewallRule -Rule $Rule
                $true
            }
            'Audit' {
                _ApplyAuditRule -Rule $Rule
                $true
            }
            'Encryption' {
                _ApplyEncryptionRule -Rule $Rule
                $true
            }
            default {
                $false
            }
        }
    }
    catch {
        Write-Log -Message "Remediation failed for rule $($Rule.Name): $($_.Exception.Message)" -Level Warning
        $false
    }
}

# Import rule application functions from Invoke-SecurityHardening
# These are used for remediation
function _ApplyRegistryRule {
    <#
    .SYNOPSIS
    Internal helper: Applies single registry hardening rule to Windows registry.
    #>

    param([PSCustomObject]$Rule)
    $regDef = $Rule.RuleDefinition

    if ($regDef.ContainsKey('Path') -and $regDef.ContainsKey('Name')) {
        $path = $regDef.Path
        $name = $regDef.Name
        $value = $regDef.Value
        $valueType = $regDef.ValueType

        if (-not (Test-Path -Path $path)) {
            New-Item -Path $path -Force | Out-Null
        }
        Set-ItemProperty -Path $path -Name $name -Value $value -Type $valueType -Force
    }
    elseif ($regDef.ContainsKey('RegKeys')) {
        foreach ($regKey in $regDef.RegKeys) {
            $path = $regKey.Path
            $name = $regKey.Name
            $value = $regKey.Value

            if (-not (Test-Path -Path $path)) {
                New-Item -Path $path -Force | Out-Null
            }
            Set-ItemProperty -Path $path -Name $name -Value $value -Force
        }
    }
}

function _ApplyServiceRule {
    <#
    .SYNOPSIS
    Internal helper: Applies single service hardening rule to Windows services and features.
    #>

    param([PSCustomObject]$Rule)
    $serviceDef = $Rule.RuleDefinition

    if ($serviceDef.ContainsKey('FeatureName')) {
        $featureName = $serviceDef.FeatureName
        $state = $serviceDef.State

        if ($state -eq 'Disabled') {
            Disable-WindowsOptionalFeature -Online -FeatureName $featureName -NoRestart -ErrorAction SilentlyContinue
        }
    }
    elseif ($serviceDef.ContainsKey('ServiceName')) {
        $serviceName = $serviceDef.ServiceName
        $startType = $serviceDef.StartType

        if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) {
            Set-Service -Name $serviceName -StartupType $startType
        }
    }
    elseif ($serviceDef.ContainsKey('Services')) {
        foreach ($svcName in $serviceDef.Services) {
            if (Get-Service -Name $svcName -ErrorAction SilentlyContinue) {
                Set-Service -Name $svcName -StartupType $serviceDef.StartType
            }
        }
    }
}

function _ApplyFirewallRule {
    <#
    .SYNOPSIS
    Internal helper: Applies single firewall hardening rule to Windows Firewall profiles.
    #>

    param([PSCustomObject]$Rule)
    $fwDef = $Rule.RuleDefinition

    if ($fwDef.ContainsKey('Profiles')) {
        foreach ($profileName in $fwDef.Profiles) {
            # Skip GpoBoolean type casting - firewall remains at default (enabled)
        }
    }
    elseif ($fwDef.ContainsKey('DefaultInboundAction')) {
        Set-NetFirewallProfile -Profile Domain, Private, Public `
            -DefaultInboundAction $fwDef.DefaultInboundAction `
            -DefaultOutboundAction $fwDef.DefaultOutboundAction -ErrorAction SilentlyContinue
    }
}

function _ApplyAuditRule {
    <#
    .SYNOPSIS
    Internal helper: Applies single audit policy rule using auditpol command.
    #>

    param([PSCustomObject]$Rule)
    $auditDef = $Rule.RuleDefinition

    if ($auditDef.ContainsKey('SubCategory')) {
        $subcategory = $auditDef.SubCategory
        $success = if ($auditDef.Success) {
            'enable'
        }
        else {
            'disable'
        }
        $failure = if ($auditDef.Failure) {
            'enable'
        }
        else {
            'disable'
        }

        auditpol /set /subcategory:"$subcategory" /success:$success /failure:$failure 2>&1 | Out-Null
    }
    elseif ($auditDef.ContainsKey('Category')) {
        $category = $auditDef.Category
        $success = if ($auditDef.Success) {
            'enable'
        }
        else {
            'disable'
        }
        $failure = if ($auditDef.Failure) {
            'enable'
        }
        else {
            'disable'
        }

        auditpol /set /category:"$category" /success:$success /failure:$failure 2>&1 | Out-Null
    }
}

function _ApplyEncryptionRule {
    <#
    .SYNOPSIS
    Internal helper: Applies single encryption hardening rule for BitLocker and drive encryption.
    #>

    param([PSCustomObject]$Rule)
    $encDef = $Rule.RuleDefinition

    if ($encDef.ContainsKey('DriveType')) {
        $driveType = $encDef.DriveType

        if ($driveType -eq 'OS') {
            $osVolume = Get-Volume -DriveLetter (Split-Path -Qualifier $env:SystemRoot) -ErrorAction SilentlyContinue
            if ($osVolume) {
                Enable-BitLocker -MountPoint "$($osVolume.DriveLetter):" -EncryptionMethod $encDef.EncryptionMethod `
                    -UsedSpaceOnly -ErrorAction SilentlyContinue | Out-Null
            }
        }
    }
}