Compliance/Test-NISTCompliance.ps1

function Test-NISTCompliance {
    <#
    .SYNOPSIS
        Tests system compliance against NIST 800-53 security controls
    .DESCRIPTION
        Evaluates Windows system against NIST (National Institute of Standards and Technology)
        SP 800-53 security and privacy controls for federal information systems
    .PARAMETER ControlFamily
        Specific control family to test: All, AccessControl, AuditAccountability,
        IdentificationAuthentication, SystemProtection, ConfigurationManagement
    .PARAMETER Revision
        NIST 800-53 revision: 4 or 5 (default: 5)
    .EXAMPLE
        Test-NISTCompliance
        Test-NISTCompliance -ControlFamily AccessControl -Revision 5
    .OUTPUTS
        PSCustomObject with NIST compliance results
    #>

    
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('All', 'AccessControl', 'AuditAccountability', 'IdentificationAuthentication', 
                     'SystemProtection', 'ConfigurationManagement')]
        [string]$ControlFamily = 'All',
        
        [Parameter()]
        [ValidateSet(4, 5)]
        [int]$Revision = 5
    )
    
    begin {
        if ($Revision -ne 5) {
            Write-Warning "Revision $Revision requested; only Revision 5 checks are currently implemented"
            $Revision = 5
        }
        Write-Host "=== NIST 800-53 COMPLIANCE TEST ===" -ForegroundColor Cyan
        Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow
        Write-Host "Revision: NIST 800-53 Rev $Revision" -ForegroundColor Yellow
        Write-Host "Control Family: $ControlFamily" -ForegroundColor Yellow
        
        $results = [PSCustomObject]@{
            TestDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            ComputerName = $env:COMPUTERNAME
            NISTRevision = $Revision
            ControlFamily = $ControlFamily
            Controls = @()
            Summary = @{
                TotalControls = 0
                Compliant = 0
                NonCompliant = 0
                PartiallyCompliant = 0
                NotApplicable = 0
                Manual = 0
                NotEvaluated = 0
                EvaluatedControls = 0
                CompliancePercentage = 0
                CoveragePercentage = 0
            }
            Recommendations = @()
        }
        
        # Helper function to test registry value
        function Test-RegistryValue {
            param(
                [string]$Path,
                [string]$Name,
                [object]$ExpectedValue
            )
            
            try {
                if (Test-Path $Path) {
                    $value = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue | 
                             Select-Object -ExpandProperty $Name -ErrorAction SilentlyContinue
                    
                    return @{
                        Compliant = ($value -eq $ExpectedValue)
                        ActualValue = $value
                        ExpectedValue = $ExpectedValue
                    }
                }
                return @{
                    Compliant = $false
                    ActualValue = "Path not found"
                    ExpectedValue = $ExpectedValue
                }
            }
            catch {
                return @{
                    Compliant = $false
                    ActualValue = "Error"
                    ExpectedValue = $ExpectedValue
                }
            }
        }
    }
    
    process {
        try {
            # 1. ACCESS CONTROL (AC) FAMILY
            if ($ControlFamily -in @('All', 'AccessControl')) {
                Write-Host "`n[1/5] Testing Access Control (AC) Controls..." -ForegroundColor Cyan
                
                try {
                    # AC-2: Account Management
                    $guestUser = Get-LocalUserByRid -Rid 501 -ErrorAction SilentlyContinue
                    $guestKnown = ($null -ne $guestUser) -and ($null -ne $guestUser.Enabled)
                    $guestDisabled = $guestKnown -and ($guestUser.Enabled -eq $false)
                    $adminUser = Get-LocalUserByRid -Rid 500 -ErrorAction SilentlyContinue
                    $adminKnown = ($null -ne $adminUser) -and ($null -ne $adminUser.Enabled)
                    $adminDisabled = $adminKnown -and ($adminUser.Enabled -eq $false)

                    $control = [PSCustomObject]@{
                        ControlID = 'AC-2'
                        ControlName = 'Account Management'
                        ControlFamily = 'AccessControl'
                        Description = 'Guest and default Administrator accounts should be disabled'
                        Expected = 'Guest and Administrator accounts disabled'
                        Actual = "Guest: $(if(-not $guestKnown){'Unknown'}elseif($guestDisabled){'Disabled'}else{'Enabled'}), Admin: $(if(-not $adminKnown){'Unknown'}elseif($adminDisabled){'Disabled'}else{'Enabled'})"
                        Status = if (-not $guestKnown -or -not $adminKnown) { 'NotEvaluated' }
                                 elseif ($guestDisabled -and $adminDisabled) { 'Compliant' }
                                 elseif ($guestDisabled -or $adminDisabled) { 'PartiallyCompliant' }
                                 else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Disable built-in Guest and Administrator accounts'
                    }
                    $results.Controls += $control
                    
                    # AC-7: Unsuccessful Logon Attempts
                    # Read the real account lockout threshold via the module helper
                    # (NetUserModalsGet/secedit); the old MaxDevicePasswordFailedAttempts
                    # registry check measured a different, device-specific policy.
                    $passwordPolicy = Get-LocalPasswordPolicy
                    $lockoutThreshold = $passwordPolicy.LockoutThreshold

                    $control = [PSCustomObject]@{
                        ControlID = 'AC-7'
                        ControlName = 'Unsuccessful Logon Attempts'
                        ControlFamily = 'AccessControl'
                        Description = 'System should lock after failed login attempts'
                        Expected = 'Account lockout threshold between 1 and 10 attempts'
                        Actual = if ($null -eq $lockoutThreshold) { 'Unable to determine' }
                                 elseif ($lockoutThreshold -eq 0) { 'Lockout disabled (0)' }
                                 else { "$lockoutThreshold attempts" }
                        Status = if ($null -eq $lockoutThreshold) { 'NotEvaluated' }
                                 elseif ($lockoutThreshold -ge 1 -and $lockoutThreshold -le 10) { 'Compliant' }
                                 else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Configure account lockout threshold between 1 and 10 failed attempts'
                    }
                    $results.Controls += $control
                    
                    # AC-11: Session Lock
                    $regTest = Test-RegistryValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
                        -Name 'InactivityTimeoutSecs' -ExpectedValue 900
                    $inactivitySecs = $regTest.ActualValue
                    $inactivityIsNumeric = ($null -ne $inactivitySecs) -and ("$inactivitySecs" -match '^\d+$')
                    # 0 means the session never locks -> NonCompliant; valid range is 1..900
                    $inactivityValid = $inactivityIsNumeric -and ([int]$inactivitySecs -gt 0) -and ([int]$inactivitySecs -le 900)
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'AC-11'
                        ControlName = 'Session Lock'
                        ControlFamily = 'AccessControl'
                        Description = 'Automatic screen lock after inactivity'
                        Expected = '15 minutes (900 seconds) or less, and not disabled (0)'
                        Actual = if (-not $inactivityIsNumeric) { "Not configured" }
                                 elseif ([int]$inactivitySecs -eq 0) { "Never locks (0)" }
                                 else { "$([math]::Round([int]$inactivitySecs/60)) minutes" }
                        Status = if ($inactivityValid) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Medium'
                        Remediation = 'Configure screen lock timeout to 15 minutes or less'
                    }
                    $results.Controls += $control
                    
                    # AC-17: Remote Access
                    $rdpSetting = Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -ErrorAction SilentlyContinue
                    $rdpEnabled = if ($rdpSetting) { $rdpSetting.fDenyTSConnections } else { $null }
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'AC-17'
                        ControlName = 'Remote Access'
                        ControlFamily = 'AccessControl'
                        Description = 'Remote access should be controlled and monitored'
                        Expected = 'RDP disabled or properly secured'
                        Actual = if ($rdpEnabled -eq 1) { "Disabled" } else { "Enabled" }
                        Status = if ($rdpEnabled -eq 1) { 'Compliant' } else { 'PartiallyCompliant' }
                        Impact = 'High'
                        Remediation = 'Disable RDP if not needed, or ensure NLA and firewall rules'
                    }
                    $results.Controls += $control
                }
                catch {
                    Write-Warning "Error testing Access Control: $_"
                }
            }
            
            # 2. AUDIT AND ACCOUNTABILITY (AU) FAMILY
            if ($ControlFamily -in @('All', 'AuditAccountability')) {
                Write-Host "`n[2/5] Testing Audit and Accountability (AU) Controls..." -ForegroundColor Cyan
                
                try {
                    # AU-2: Audit Events
                    $logonGuid = Get-AuditSubcategoryGuid -Name 'Logon'
                    $logonAuditValue = $null
                    if ($null -ne $logonGuid) {
                        $logonTable = Get-AuditPolicyTable -Subcategory 'Logon'
                        $logonKey = ($logonGuid -replace '[{}]', '').ToLowerInvariant()
                        if ($logonTable.ContainsKey($logonKey)) { $logonAuditValue = $logonTable[$logonKey].Value }
                    }

                    $control = [PSCustomObject]@{
                        ControlID = 'AU-2'
                        ControlName = 'Audit Events'
                        ControlFamily = 'AuditAccountability'
                        Description = 'System must audit significant security events'
                        Expected = 'Audit policies configured'
                        Actual = if ($null -eq $logonAuditValue) { 'Unable to determine' }
                                 elseif ($logonAuditValue -ge 1 -and $logonAuditValue -le 3) { "Configured" } else { "Not configured" }
                        Status = if ($null -eq $logonAuditValue) { 'NotEvaluated' }
                                 elseif ($logonAuditValue -ge 1 -and $logonAuditValue -le 3) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Enable audit policies for security events'
                    }
                    $results.Controls += $control
                    
                    # AU-3: Content of Audit Records
                    $winEventAvailable = $null -ne (Get-Command Get-WinEvent -ErrorAction SilentlyContinue)
                    $securityLog = $null
                    if ($winEventAvailable) {
                        try { $securityLog = Get-WinEvent -ListLog Security -ErrorAction Stop } catch { $securityLog = $null }
                    }
                    $eventLogSize = if ($securityLog) { $securityLog.MaximumSizeInBytes } else { 0 }
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'AU-3'
                        ControlName = 'Content of Audit Records'
                        ControlFamily = 'AuditAccountability'
                        Description = 'Audit records must contain sufficient detail'
                        Expected = 'Security log size >= 100MB'
                        Actual = if (-not $winEventAvailable) { 'Unable to evaluate — Get-WinEvent unavailable' }
                                 else { "$([math]::Round($eventLogSize/1MB)) MB" }
                        Status = if (-not $winEventAvailable) { 'NotEvaluated' }
                                 elseif ($eventLogSize -ge 100MB) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Medium'
                        Remediation = 'Increase Security event log size to at least 100MB'
                    }
                    $results.Controls += $control
                    
                    # AU-9: Protection of Audit Information
                    $logPath = "C:\Windows\System32\winevt\Logs\Security.evtx"
                    if (Test-Path $logPath) {
                        $acl = $null
                        try { $acl = Get-Acl $logPath -ErrorAction Stop } catch { $acl = $null }

                        if ($null -eq $acl) {
                            # No ACL data -> honest unknown; never claim Compliant without data
                            $control = [PSCustomObject]@{
                                ControlID = 'AU-9'
                                ControlName = 'Protection of Audit Information'
                                ControlFamily = 'AuditAccountability'
                                Description = 'Audit logs must be protected from unauthorized access'
                                Expected = 'Security log protected from normal users'
                                Actual = 'Unable to read ACL'
                                Status = 'NotEvaluated'
                                Impact = 'High'
                                Remediation = 'Restrict Security event log access to administrators only'
                            }
                        }
                        else {
                            $protected = @($acl.Access | Where-Object { ((Test-IdentityReferenceIsSid -IdentityReference $_.IdentityReference -Sid 'S-1-5-32-545') -or (Test-IdentityReferenceIsSid -IdentityReference $_.IdentityReference -Sid 'S-1-5-11') -or (Test-IdentityReferenceIsSid -IdentityReference $_.IdentityReference -Sid 'S-1-1-0')) -and $_.FileSystemRights -match "FullControl|Modify|Write" })

                            $control = [PSCustomObject]@{
                                ControlID = 'AU-9'
                                ControlName = 'Protection of Audit Information'
                                ControlFamily = 'AuditAccountability'
                                Description = 'Audit logs must be protected from unauthorized access'
                                Expected = 'Security log protected from normal users'
                                Actual = if ($protected.Count -gt 0) { "Weak permissions detected" } else { "Properly protected" }
                                Status = if ($protected.Count -eq 0) { 'Compliant' } else { 'NonCompliant' }
                                Impact = 'High'
                                Remediation = 'Restrict Security event log access to administrators only'
                            }
                        }
                        $results.Controls += $control
                    }
                    
                    # AU-12: Audit Generation
                    $processCreationGuid = Get-AuditSubcategoryGuid -Name 'Process Creation'
                    $processAuditEntry = $null
                    if ($null -ne $processCreationGuid) {
                        $processTable = Get-AuditPolicyTable -Subcategory 'Process Creation'
                        $processKey = ($processCreationGuid -replace '[{}]', '').ToLowerInvariant()
                        if ($processTable.ContainsKey($processKey)) { $processAuditEntry = $processTable[$processKey] }
                    }

                    $control = [PSCustomObject]@{
                        ControlID = 'AU-12'
                        ControlName = 'Audit Generation'
                        ControlFamily = 'AuditAccountability'
                        Description = 'System must generate audit records for defined events'
                        Expected = 'Process creation auditing enabled'
                        Actual = if ($null -eq $processAuditEntry -or $null -eq $processAuditEntry.Value) { 'Unable to determine' }
                                 elseif ($processAuditEntry.Success) { "Enabled" } else { "Disabled" }
                        Status = if ($null -eq $processAuditEntry -or $null -eq $processAuditEntry.Value) { 'NotEvaluated' }
                                 elseif ($processAuditEntry.Success) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Medium'
                        Remediation = 'Enable process creation auditing'
                    }
                    $results.Controls += $control
                }
                catch {
                    Write-Warning "Error testing Audit and Accountability: $_"
                }
            }
            
            # 3. IDENTIFICATION AND AUTHENTICATION (IA) FAMILY
            if ($ControlFamily -in @('All', 'IdentificationAuthentication')) {
                Write-Host "`n[3/5] Testing Identification and Authentication (IA) Controls..." -ForegroundColor Cyan
                
                try {
                    # IA-2: Identification and Authentication
                    $passwordPolicy = Get-LocalPasswordPolicy
                    $minPasswordLength = $passwordPolicy.MinPasswordLength

                    $control = [PSCustomObject]@{
                        ControlID = 'IA-2'
                        ControlName = 'Identification and Authentication'
                        ControlFamily = 'IdentificationAuthentication'
                        Description = 'System must uniquely identify and authenticate users'
                        Expected = 'Minimum password length >= 14 characters'
                        Actual = if ($null -eq $minPasswordLength) { 'Unable to determine' }
                                 else { "$minPasswordLength characters" }
                        Status = if ($null -eq $minPasswordLength) { 'NotEvaluated' }
                                 elseif ($minPasswordLength -ge 14) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Set minimum password length to 14 characters'
                    }
                    $results.Controls += $control

                    # IA-5: Authenticator Management
                    $maxAgeDays = $passwordPolicy.MaxPasswordAgeDays

                    $control = [PSCustomObject]@{
                        ControlID = 'IA-5'
                        ControlName = 'Authenticator Management'
                        ControlFamily = 'IdentificationAuthentication'
                        Description = 'Authenticators must be managed securely'
                        Expected = 'Maximum password age <= 60 days'
                        Actual = if ($null -eq $maxAgeDays) { 'Unable to determine' }
                                 elseif ($maxAgeDays -eq -1) { 'Never expires' } else { "$maxAgeDays days" }
                        Status = if ($null -eq $maxAgeDays) { 'NotEvaluated' }
                                 elseif ($maxAgeDays -gt 0 -and $maxAgeDays -le 60) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Set maximum password age to 60 days or less'
                    }
                    $results.Controls += $control

                    # IA-8: Identification and Authentication (Non-Organizational Users)
                    $regTest = Test-RegistryValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' `
                        -Name 'LimitBlankPasswordUse' -ExpectedValue 1
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'IA-8'
                        ControlName = 'Non-Organizational User Authentication'
                        ControlFamily = 'IdentificationAuthentication'
                        Description = 'Blank passwords should not be allowed'
                        Expected = 'Blank passwords disabled'
                        Actual = if ($regTest.Compliant) { "Disabled" } else { "Allowed" }
                        Status = if ($regTest.Compliant) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Critical'
                        Remediation = 'Disable blank password usage'
                    }
                    $results.Controls += $control
                }
                catch {
                    Write-Warning "Error testing Identification and Authentication: $_"
                }
            }
            
            # 4. SYSTEM AND COMMUNICATIONS PROTECTION (SC) FAMILY
            if ($ControlFamily -in @('All', 'SystemProtection')) {
                Write-Host "`n[4/5] Testing System and Communications Protection (SC) Controls..." -ForegroundColor Cyan
                
                try {
                    # SC-7: Boundary Protection - FIX: Wrap in @()
                    # CommandNotFoundException ignores -ErrorAction SilentlyContinue, so
                    # gate on Get-Command first; a missing cmdlet must not drop the control.
                    $firewallCmdletAvailable = $null -ne (Get-Command Get-NetFirewallProfile -ErrorAction SilentlyContinue)
                    $firewallProfiles = $null
                    if ($firewallCmdletAvailable) {
                        try { $firewallProfiles = Get-NetFirewallProfile -ErrorAction Stop } catch { $firewallProfiles = $null }
                    }
                    $disabledFirewalls = @($firewallProfiles | Where-Object { $null -ne $_ -and -not $_.Enabled })
                    $allFirewallsEnabled = ($null -ne $firewallProfiles) -and ($disabledFirewalls.Count -eq 0)
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'SC-7'
                        ControlName = 'Boundary Protection'
                        ControlFamily = 'SystemProtection'
                        Description = 'Firewall must protect system boundaries'
                        Expected = 'All firewall profiles enabled'
                        Actual = if (-not $firewallCmdletAvailable) { 'Unable to evaluate — Get-NetFirewallProfile unavailable' }
                                 elseif ($null -eq $firewallProfiles) { "Unable to retrieve firewall profiles" }
                                 elseif ($allFirewallsEnabled) { "All enabled" } else { "Some disabled" }
                        Status = if (-not $firewallCmdletAvailable) { 'NotEvaluated' }
                                 elseif ($null -eq $firewallProfiles) { 'NotEvaluated' }
                                 elseif ($allFirewallsEnabled) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Critical'
                        Remediation = 'Enable Windows Firewall for all profiles'
                    }
                    $results.Controls += $control
                    
                    # SC-8: Transmission Confidentiality and Integrity
                    $regTest = Test-RegistryValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' `
                        -Name 'NoLMHash' -ExpectedValue 1
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'SC-8'
                        ControlName = 'Transmission Confidentiality'
                        ControlFamily = 'SystemProtection'
                        Description = 'Weak LAN Manager hashes should not be stored'
                        Expected = 'LM hash storage disabled'
                        Actual = if ($regTest.Compliant) { "Disabled" } else { "Enabled" }
                        Status = if ($regTest.Compliant) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Disable LAN Manager hash storage'
                    }
                    $results.Controls += $control
                    
                    # SC-28: Protection of Information at Rest
                    # Gate on Get-Command: CommandNotFoundException ignores
                    # -ErrorAction SilentlyContinue and would drop this control via the
                    # family-level catch.
                    $bitlockerAvailable = $null -ne (Get-Command Get-BitLockerVolume -ErrorAction SilentlyContinue)
                    $bitlockerStatus = $null
                    if ($bitlockerAvailable) {
                        try { $bitlockerStatus = Get-BitLockerVolume -MountPoint "C:" -ErrorAction Stop } catch { $bitlockerStatus = $null }
                    }
                    $bitlockerEnabled = ($null -ne $bitlockerStatus) -and ($bitlockerStatus.ProtectionStatus -eq 'On')
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'SC-28'
                        ControlName = 'Protection of Information at Rest'
                        ControlFamily = 'SystemProtection'
                        Description = 'Data at rest should be encrypted'
                        Expected = 'BitLocker enabled'
                        Actual = if (-not $bitlockerAvailable) { 'Unable to evaluate — Get-BitLockerVolume unavailable' }
                                 elseif ($bitlockerEnabled) { "Enabled" } else { "Disabled" }
                        Status = if (-not $bitlockerAvailable) { 'NotEvaluated' }
                                 elseif ($bitlockerEnabled) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'High'
                        Remediation = 'Enable BitLocker encryption'
                    }
                    $results.Controls += $control
                }
                catch {
                    Write-Warning "Error testing System Protection: $_"
                }
            }
            
            # 5. CONFIGURATION MANAGEMENT (CM) FAMILY
            if ($ControlFamily -in @('All', 'ConfigurationManagement')) {
                Write-Host "`n[5/5] Testing Configuration Management (CM) Controls..." -ForegroundColor Cyan
                
                try {
                    # CM-6: Configuration Settings
                    # Gate on Get-Command: CommandNotFoundException ignores
                    # -ErrorAction SilentlyContinue and would drop CM-6/7/8 via the
                    # family-level catch.
                    $mpStatusAvailable = $null -ne (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue)
                    $defenderStatus = $null
                    if ($mpStatusAvailable) {
                        try { $defenderStatus = Get-MpComputerStatus -ErrorAction Stop } catch { $defenderStatus = $null }
                    }
                    $defenderEnabled = ($null -ne $defenderStatus) -and $defenderStatus.AntivirusEnabled
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'CM-6'
                        ControlName = 'Configuration Settings'
                        ControlFamily = 'ConfigurationManagement'
                        Description = 'Security configuration settings must be established'
                        Expected = 'Windows Defender enabled'
                        Actual = if (-not $mpStatusAvailable) { 'Unable to evaluate — Get-MpComputerStatus unavailable' }
                                 elseif ($defenderEnabled) { "Enabled" } else { "Disabled" }
                        Status = if (-not $mpStatusAvailable) { 'NotEvaluated' }
                                 elseif ($defenderEnabled) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Critical'
                        Remediation = 'Enable Windows Defender antivirus'
                    }
                    $results.Controls += $control
                    
                    # CM-7: Least Functionality - FIX: Wrap in @()
                    # Get-Service is Windows/Linux-only; gate on Get-Command so a
                    # missing cmdlet cannot drop CM-7/CM-8 via the family catch.
                    $serviceCmdletAvailable = $null -ne (Get-Command Get-Service -ErrorAction SilentlyContinue)
                    $allServices = $null
                    if ($serviceCmdletAvailable) {
                        try { $allServices = Get-Service -ErrorAction Stop } catch { $allServices = $null }
                    }
                    $unnecessaryServices = @($allServices | Where-Object { 
                        $null -ne $_ -and $_.Name -in @('TlntSvr', 'SNMP', 'SSDPSRV') -and $_.Status -eq 'Running'
                    })
                    
                    $control = [PSCustomObject]@{
                        ControlID = 'CM-7'
                        ControlName = 'Least Functionality'
                        ControlFamily = 'ConfigurationManagement'
                        Description = 'Unnecessary services should be disabled'
                        Expected = 'No unnecessary services running'
                        Actual = if (-not $serviceCmdletAvailable) { 'Unable to evaluate — Get-Service unavailable' }
                                 elseif ($unnecessaryServices.Count -eq 0) { "Clean" } else { "$($unnecessaryServices.Count) unnecessary services" }
                        Status = if (-not $serviceCmdletAvailable) { 'NotEvaluated' }
                                 elseif ($unnecessaryServices.Count -eq 0) { 'Compliant' } else { 'NonCompliant' }
                        Impact = 'Medium'
                        Remediation = 'Disable unnecessary services (Telnet, SNMP, SSDP)'
                    }
                    $results.Controls += $control
                    
                    # CM-8: Information System Component Inventory
                    $control = [PSCustomObject]@{
                        ControlID = 'CM-8'
                        ControlName = 'System Component Inventory'
                        ControlFamily = 'ConfigurationManagement'
                        Description = 'Maintain accurate inventory of system components'
                        Expected = 'Inventory maintained'
                        Actual = 'Manual verification required'
                        Status = 'Manual'
                        Impact = 'Low'
                        Remediation = 'Implement automated inventory management'
                    }
                    $results.Controls += $control
                }
                catch {
                    Write-Warning "Error testing Configuration Management: $_"
                }
            }
            
            # Calculate summary - FIX: All wrapped in @()
            $results.Summary.TotalControls = $results.Controls.Count
            $results.Summary.Compliant = @($results.Controls | Where-Object { $_.Status -eq 'Compliant' }).Count
            $results.Summary.NonCompliant = @($results.Controls | Where-Object { $_.Status -eq 'NonCompliant' }).Count
            $results.Summary.PartiallyCompliant = @($results.Controls | Where-Object { $_.Status -eq 'PartiallyCompliant' }).Count
            $results.Summary.NotApplicable = @($results.Controls | Where-Object { $_.Status -eq 'NotApplicable' }).Count
            $results.Summary.Manual = @($results.Controls | Where-Object { $_.Status -eq 'Manual' }).Count
            $results.Summary.NotEvaluated = @($results.Controls | Where-Object { $_.Status -eq 'NotEvaluated' }).Count

            # NotEvaluated stays in the denominator and never counts as compliant;
            # only NotApplicable and Manual leave the denominator.
            $scopedControls = $results.Summary.TotalControls - $results.Summary.NotApplicable - $results.Summary.Manual
            $evaluatedControls = $scopedControls - $results.Summary.NotEvaluated
            $results.Summary.EvaluatedControls = $evaluatedControls
            if ($scopedControls -gt 0) {
                $compliantCount = $results.Summary.Compliant + ($results.Summary.PartiallyCompliant * 0.5)
                $results.Summary.CompliancePercentage = [math]::Round(($compliantCount / $scopedControls) * 100, 2)
                $results.Summary.CoveragePercentage = [math]::Round(($evaluatedControls / $scopedControls) * 100, 2)
            }
            else {
                $results.Summary.CompliancePercentage = $null
                $results.Summary.CoveragePercentage = $null
            }

            if ($results.Summary.NotEvaluated -gt 0) {
                $notEvaluatedIds = @($results.Controls | Where-Object { $_.Status -eq 'NotEvaluated' } | ForEach-Object { $_.ControlID }) -join ', '
                Write-Warning "$($results.Summary.NotEvaluated) control(s) could not be evaluated and are counted as not compliant: $notEvaluatedIds"
            }
            
            # Generate recommendations - FIX: All wrapped in @()
            $criticalFailed = @($results.Controls | Where-Object { $_.Status -eq 'NonCompliant' -and $_.Impact -eq 'Critical' })
            $highFailed = @($results.Controls | Where-Object { $_.Status -eq 'NonCompliant' -and $_.Impact -eq 'High' })
            
            if ($criticalFailed.Count -gt 0) {
                $results.Recommendations += "CRITICAL: $($criticalFailed.Count) critical control(s) non-compliant"
                $results.Recommendations += "Address critical controls immediately"
            }
            
            if ($highFailed.Count -gt 0) {
                $results.Recommendations += "HIGH: $($highFailed.Count) high-impact control(s) non-compliant"
            }

            foreach ($notEvaluatedControl in @($results.Controls | Where-Object { $_.Status -eq 'NotEvaluated' })) {
                $results.Recommendations += "Could not evaluate $($notEvaluatedControl.ControlID): run elevated or install the required feature/module"
            }
            
            if ($results.Summary.CompliancePercentage -lt 80) {
                $results.Recommendations += "Compliance below 80% - implement remediation plan"
            }
            
            $results.Recommendations += "Review all non-compliant controls"
            $results.Recommendations += "Document compensating controls where applicable"
            
        }
        catch {
            Write-Error "Error during NIST compliance testing: $_"
            throw
        }
    }
    
    end {
        $complianceDisplay = if ($null -eq $results.Summary.CompliancePercentage) { 'N/A' } else { "$($results.Summary.CompliancePercentage)%" }
        $coverageDisplay = if ($null -eq $results.Summary.CoveragePercentage) { 'N/A' } else { "$($results.Summary.CoveragePercentage)%" }

        Write-Host "`n=== COMPLIANCE TEST COMPLETE ===" -ForegroundColor $(
            if ($results.Summary.CompliancePercentage -ge 80) { 'Green' }
            elseif ($results.Summary.CompliancePercentage -ge 60) { 'Yellow' }
            else { 'Red' }
        )

        Write-Host "`nCompliance Summary:" -ForegroundColor Cyan
        Write-Host " Total Controls Tested: $($results.Summary.TotalControls)" -ForegroundColor White
        Write-Host " Compliant: $($results.Summary.Compliant)" -ForegroundColor Green
        Write-Host " Non-Compliant: $($results.Summary.NonCompliant)" -ForegroundColor Red
        Write-Host " Partially Compliant: $($results.Summary.PartiallyCompliant)" -ForegroundColor Yellow
        Write-Host " Not Applicable: $($results.Summary.NotApplicable)" -ForegroundColor Gray
        Write-Host " Manual: $($results.Summary.Manual)" -ForegroundColor Yellow
        Write-Host " Not Evaluated: $($results.Summary.NotEvaluated)" -ForegroundColor Yellow
        Write-Host " Compliance: $complianceDisplay (coverage $coverageDisplay, $($results.Summary.NotEvaluated) not evaluated)" -ForegroundColor $(
            if ($results.Summary.CompliancePercentage -ge 80) { 'Green' }
            elseif ($results.Summary.CompliancePercentage -ge 60) { 'Yellow' }
            else { 'Red' }
        )
        
        if ($results.Controls.Count -gt 0) {
            $nonCompliant = @($results.Controls | Where-Object { $_.Status -eq 'NonCompliant' })
            if ($nonCompliant.Count -gt 0) {
                Write-Host "`nNon-Compliant Controls by Impact:" -ForegroundColor Cyan
                $failedByImpact = $nonCompliant | Group-Object Impact
                foreach ($group in $failedByImpact | Sort-Object Name) {
                    Write-Host " $($group.Name): $($group.Count)" -ForegroundColor $(
                        switch ($group.Name) {
                            'Critical' { 'Red' }
                            'High' { 'Red' }
                            'Medium' { 'Yellow' }
                            default { 'White' }
                        }
                    )
                }
            }
        }
        
        if ($results.Recommendations.Count -gt 0) {
            Write-Host "`nRecommendations:" -ForegroundColor Cyan
            $results.Recommendations | ForEach-Object {
                Write-Host " ! $_" -ForegroundColor Yellow
            }
        }
        
        Write-Host "`nNIST SP 800-53: https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final" -ForegroundColor Gray
        
        return $results
    }
}