tests/New-SecurityDriftReport.Tests.ps1

BeforeAll {
    $modulePath = (Resolve-Path "$PSScriptRoot\..\modules\System.psm1").Path
    Import-Module $modulePath -Force

    # Setup test fixtures directory
    $fixturesDir = "$PSScriptRoot\fixtures"
    if (-not (Test-Path $fixturesDir)) {
        New-Item -ItemType Directory -Path $fixturesDir -Force | Out-Null
    }

    # Create sample drift findings for testing
    $script:testDriftFindings = @(
        [PSCustomObject]@{
            Finding = "Password expiration not configured"
            Severity = "CRITICAL"
            Category = "Account Policies"
            Current = "Disabled"
            Expected = "90 days"
            Remediation = "Configure password expiration policy"
        },
        [PSCustomObject]@{
            Finding = "SMB signing not enforced"
            Severity = "HIGH"
            Category = "Network Security"
            Current = "Not enforced"
            Expected = "Required"
            Remediation = "Enable SMB signing via registry"
        },
        [PSCustomObject]@{
            Finding = "RDP NLA disabled"
            Severity = "HIGH"
            Category = "RDP Security"
            Current = "Disabled"
            Expected = "Enabled"
            Remediation = "Enable RDP NLA via registry"
        },
        [PSCustomObject]@{
            Finding = "Windows Update auto-install disabled"
            Severity = "MEDIUM"
            Category = "Updates"
            Current = "Manual"
            Expected = "Auto-Install"
            Remediation = "Configure auto-install policy"
        }
    )

    # Create empty findings for compliant test
    $script:emptyDriftFindings = @()
}

AfterAll {
    Remove-Module System -Force -ErrorAction SilentlyContinue
}

Describe "New-SecurityDriftReport" {
    Context "Parameter Validation" {
        It "accepts DriftFindings parameter as array" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $result | Should -Not -BeNullOrEmpty
        }

        It "accepts DriftFindings as single object" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings[0] -ErrorAction Stop
            $result | Should -Not -BeNullOrEmpty
        }

        It "accepts OutputDirectory parameter" {
            $testTempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "DriftTest_$(Get-Random)")
            New-Item -ItemType Directory -Path $testTempDir -Force | Out-Null
            try {
                $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop
                $result.ReportPath | Should -Match ([regex]::Escape($testTempDir))
            }
            finally {
                Remove-Item -Path $testTempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }

        It "uses default logs directory when OutputDirectory not specified" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $result.ReportPath | Should -Match "logs"
        }
    }

    Context "Report Generation - Basic Output" {
        It "returns PSCustomObject with required properties" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $result.PSObject.Properties.Name | Should -Contain "ReportPath"
            $result.PSObject.Properties.Name | Should -Contain "Status"
            $result.PSObject.Properties.Name | Should -Contain "DriftCount"
            $result.PSObject.Properties.Name | Should -Contain "Severity"
        }

        It "generates report path with timestamp" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $result.ReportPath | Should -Match "Drift_Detection_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.csv"
        }

        It "sets status to NON-COMPLIANT when findings exist" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $result.Status | Should -Be "NON-COMPLIANT"
            $result.DriftCount | Should -Be 4
        }

        It "sets status to COMPLIANT when no findings" {
            $result = New-SecurityDriftReport -DriftFindings $emptyDriftFindings -ErrorAction Stop
            $result.Status | Should -Be "COMPLIANT"
            $result.DriftCount | Should -Be 0
        }
    }

    Context "Severity Calculation" {
        It "calculates overall severity as CRITICAL when critical findings exist" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $result.Severity | Should -Be "CRITICAL"
        }

        It "calculates overall severity as HIGH when only high findings exist" {
            $findings = @($testDriftFindings | Where-Object Severity -ne "CRITICAL")
            $result = New-SecurityDriftReport -DriftFindings $findings -ErrorAction Stop
            $result.Severity | Should -Be "HIGH"
        }

        It "calculates overall severity as MEDIUM when only medium findings exist" {
            $findings = @($testDriftFindings | Where-Object Severity -eq "MEDIUM")
            $result = New-SecurityDriftReport -DriftFindings $findings -ErrorAction Stop
            $result.Severity | Should -Be "MEDIUM"
        }

        It "sets severity based on highest severity present" {
            $findings = @(
                [PSCustomObject]@{ Severity = "MEDIUM" },
                [PSCustomObject]@{ Severity = "MEDIUM" },
                [PSCustomObject]@{ Severity = "HIGH" }
            )
            $result = New-SecurityDriftReport -DriftFindings $findings -ErrorAction Stop
            $result.Severity | Should -Be "HIGH"
        }
    }

    Context "CSV File Creation" {
        BeforeEach {
            $testTempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "DriftReport_$(Get-Random)")
            New-Item -ItemType Directory -Path $testTempDir -Force | Out-Null
        }

        AfterEach {
            if (Test-Path $testTempDir) {
                Remove-Item -Path $testTempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }

        It "creates CSV report file" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop
            Test-Path $result.ReportPath | Should -Be $true
            $result.ReportPath | Should -Match "\.csv$"
        }

        It "creates output directory if not exists" {
            $newDir = [System.IO.Path]::Combine($testTempDir, "NewReports")
            Test-Path $newDir | Should -Be $false

            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $newDir -ErrorAction Stop

            Test-Path $newDir | Should -Be $true
            Test-Path $result.ReportPath | Should -Be $true
        }

        It "CSV file contains summary and findings" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            $csvContent = Import-Csv $result.ReportPath
            $csvContent | Should -Not -BeNullOrEmpty
            $csvContent.Count | Should -BeGreaterThan 0
        }

        It "CSV includes hostname in summary" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            $csvContent = Import-Csv $result.ReportPath
            $summaryRow = $csvContent | Select-Object -First 1
            $summaryRow.Hostname | Should -Be $env:COMPUTERNAME
        }

        It "CSV includes status in summary" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            $csvContent = Import-Csv $result.ReportPath
            $summaryRow = $csvContent | Select-Object -First 1
            $summaryRow.Status | Should -Be "NON-COMPLIANT"
        }

        It "CSV includes drift count summary" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            $csvContent = Import-Csv $result.ReportPath
            $summaryRow = $csvContent | Select-Object -First 1
            $summaryRow.Total_Drifts | Should -Be "4"
        }

        It "CSV includes severity breakdown (CRITICAL, HIGH, MEDIUM)" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            $csvContent = Import-Csv $result.ReportPath
            $summaryRow = $csvContent | Select-Object -First 1
            $summaryRow.Critical_Count | Should -Be "1"
            $summaryRow.High_Count | Should -Be "2"
            $summaryRow.Medium_Count | Should -Be "1"
        }

        It "appends detailed findings to CSV after summary" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            $csvContent = Import-Csv $result.ReportPath
            # First row is summary, next rows should be findings
            $csvContent.Count | Should -Be 5  # 1 summary + 4 findings
        }

        It "handles empty findings (compliant system)" {
            $result = New-SecurityDriftReport -DriftFindings $emptyDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop

            Test-Path $result.ReportPath | Should -Be $true
            $csvContent = @(Import-Csv $result.ReportPath)
            $csvContent[0].Status | Should -Be "COMPLIANT"
            $csvContent.Count | Should -Be 1  # Only summary, no findings
        }
    }

    Context "WhatIf Support" {
        BeforeEach {
            $testTempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "DriftReport_$(Get-Random)")
            New-Item -ItemType Directory -Path $testTempDir -Force | Out-Null
        }

        AfterEach {
            if (Test-Path $testTempDir) {
                Remove-Item -Path $testTempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }

        It "accepts -WhatIf parameter" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -WhatIf
            $result | Should -Not -BeNullOrEmpty
        }

        It "does not create report file with -WhatIf" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -WhatIf -ErrorAction SilentlyContinue
            # With WhatIf, file should not be created
            if ($result.ReportPath) {
                Test-Path $result.ReportPath | Should -Be $false
            }
        }

        It "still returns object with -WhatIf" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -WhatIf -ErrorAction SilentlyContinue
            $result | Should -Not -BeNullOrEmpty
        }
    }

    Context "Error Handling" {
        It "handles inaccessible output directory gracefully" {
            $invalidDir = "Z:\NonExistentDrive\Path"
            { New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $invalidDir -ErrorAction Stop } | Should -Throw
        }

        It "logs errors via Write-Log on invalid directory" {
            { New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory "Z:\InvalidPath" -ErrorAction Stop } |
                Should -Throw
        }
    }

    Context "Edge Cases" {
        It "handles single finding" {
            $singleFinding = @($testDriftFindings[0])
            $result = New-SecurityDriftReport -DriftFindings $singleFinding -ErrorAction Stop
            $result.DriftCount | Should -Be 1
            $result.Status | Should -Be "NON-COMPLIANT"
        }

        It "handles findings with special characters in values" {
            $findings = @(
                [PSCustomObject]@{
                    Finding = "Test with 'quotes' and `"double quotes`""
                    Severity = "HIGH"
                    Category = "Test"
                    Current = "Value with, comma"
                    Expected = "Expected value"
                    Remediation = "Remediation steps"
                }
            )
            $result = New-SecurityDriftReport -DriftFindings $findings -ErrorAction Stop
            $result.Status | Should -Be "NON-COMPLIANT"
        }

        It "handles large number of findings" {
            $largeFindingSet = @()
            for ($i = 1; $i -le 100; $i++) {
                $largeFindingSet += [PSCustomObject]@{
                    Finding = "Finding $i"
                    Severity = @("CRITICAL", "HIGH", "MEDIUM")[(($i - 1) % 3)]
                    Category = "Test Category"
                    Current = "Current Value"
                    Expected = "Expected Value"
                    Remediation = "Fix"
                }
            }
            $result = New-SecurityDriftReport -DriftFindings $largeFindingSet -ErrorAction Stop
            $result.DriftCount | Should -Be 100
        }

        It "uses correct timestamp format in report filename" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -ErrorAction Stop
            $filename = [System.IO.Path]::GetFileName($result.ReportPath)
            $filename | Should -Match "Drift_Detection_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.csv"
        }

        It "generates unique filenames for multiple reports in same directory" {
            $reportTempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "DriftReportTest_$(Get-Random)")
            New-Item -ItemType Directory -Path $reportTempDir -Force | Out-Null
            try {
                $result1 = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $reportTempDir -ErrorAction Stop
                Start-Sleep -Seconds 1.1  # Ensure timestamp differs (reports use second-level precision)

                $result2 = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $reportTempDir -ErrorAction Stop

                $result1.ReportPath | Should -Not -Be $result2.ReportPath
                Test-Path $result1.ReportPath | Should -Be $true
                Test-Path $result2.ReportPath | Should -Be $true
            }
            finally {
                Remove-Item -Path $reportTempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
    }

    Context "Compliance Reports" {
        BeforeEach {
            $testTempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "DriftReport_$(Get-Random)")
            New-Item -ItemType Directory -Path $testTempDir -Force | Out-Null
        }

        AfterEach {
            if (Test-Path $testTempDir) {
                Remove-Item -Path $testTempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }

        It "generates COMPLIANT report when no drifts detected" {
            $result = New-SecurityDriftReport -DriftFindings @() -OutputDirectory $testTempDir -ErrorAction Stop
            $result.Status | Should -Be "COMPLIANT"
            $result.Severity | Should -Match "CRITICAL|HIGH|MEDIUM"  # Some default when empty
        }

        It "includes Overall_Severity in summary" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop
            $csvContent = Import-Csv $result.ReportPath
            $summaryRow = $csvContent | Select-Object -First 1
            $summaryRow.Overall_Severity | Should -Be "CRITICAL"
        }

        It "includes timestamp in report summary" {
            $result = New-SecurityDriftReport -DriftFindings $testDriftFindings -OutputDirectory $testTempDir -ErrorAction Stop
            $csvContent = Import-Csv $result.ReportPath
            $summaryRow = $csvContent | Select-Object -First 1
            $summaryRow.Scan_Date | Should -Not -BeNullOrEmpty
            $summaryRow.Scan_Date | Should -Match "\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"
        }
    }

    Context "Documentation" {
        It "has complete help documentation" {
            $help = Get-Help New-SecurityDriftReport
            $help.Synopsis | Should -Not -BeNullOrEmpty
        }

        It "documents DriftFindings parameter" {
            $help = Get-Help New-SecurityDriftReport
            $help.Parameters.Parameter.Name | Should -Contain "DriftFindings"
        }

        It "documents OutputDirectory parameter" {
            $help = Get-Help New-SecurityDriftReport
            $help.Parameters.Parameter.Name | Should -Contain "OutputDirectory"
        }

        It "includes usage examples" {
            $help = Get-Help New-SecurityDriftReport
            $help.Examples | Should -Not -BeNullOrEmpty
        }
    }
}