Tests/BSI.AzCompliance.Tests.ps1
|
#Requires -Version 5.1 # --- Resolve paths --- $testFile = if ($PSCommandPath) { $PSCommandPath } elseif ($PSScriptRoot) { Join-Path $PSScriptRoot 'BSI.AzCompliance.Tests.ps1' } else { $MyInvocation.MyCommand.Path } $testDir = Split-Path -Parent $testFile $moduleRoot = Split-Path -Parent $testDir # Import module (no using module needed — New-BsiComplianceResult handles class creation) Import-Module (Join-Path $moduleRoot 'BSI.AzCompliance.psd1') -Force # ============================================================ # ENUMS # ============================================================ Describe 'BsiSeverity Enum' { It 'defines all severity levels' { [enum]::GetNames([BsiSeverity]) | Should -Contain 'Critical' [enum]::GetNames([BsiSeverity]) | Should -Contain 'High' [enum]::GetNames([BsiSeverity]) | Should -Contain 'Medium' [enum]::GetNames([BsiSeverity]) | Should -Contain 'Low' [enum]::GetNames([BsiSeverity]) | Should -Contain 'Info' } } Describe 'BsiCheckStatus Enum' { It 'defines Pass, Fail, Skip, Error' { [enum]::GetNames([BsiCheckStatus]) | Should -Contain 'Pass' [enum]::GetNames([BsiCheckStatus]) | Should -Contain 'Fail' [enum]::GetNames([BsiCheckStatus]) | Should -Contain 'Skip' [enum]::GetNames([BsiCheckStatus]) | Should -Contain 'Error' } } # ============================================================ # COMPLIANCE RESULT (via New-BsiComplianceResult helper) # ============================================================ Describe 'ComplianceResult class' { It 'creates with default constructor' { $r = New-BsiComplianceResult $r.Severity | Should -Be ([BsiSeverity]::Medium) $r.Status | Should -Be ([BsiCheckStatus]::Fail) $r.Baseline | Should -Be ([BsiBaseline]::B) $r.Timestamp | Should -Not -BeNullOrEmpty } It 'creates with parameterized constructor' { $r = New-BsiComplianceResult -ControlId 'ARCH.5.2' -Title 'NSG Deny Rules' ` -Category 'Network' -Mode ([BsiCheckMode]::Remote) ` -Status ([BsiCheckStatus]::Pass) -Severity ([BsiSeverity]::High) ` -Details 'All NSGs deny SSH' $r.ControlId | Should -Be 'ARCH.5.2' $r.ControlTitle | Should -Be 'NSG Deny Rules' $r.Category | Should -Be 'Network' $r.Mode | Should -Be ([BsiCheckMode]::Remote) $r.Status | Should -Be ([BsiCheckStatus]::Pass) $r.Severity | Should -Be ([BsiSeverity]::High) $r.Details | Should -Be 'All NSGs deny SSH' } It 'IsPass returns true for Pass status' { $r = New-BsiComplianceResult $r.Status = [BsiCheckStatus]::Pass $r.IsPass() | Should -BeTrue } It 'IsPass returns false for Fail status' { $r = New-BsiComplianceResult $r.Status = [BsiCheckStatus]::Fail $r.IsPass() | Should -BeFalse } It 'ToSarifResult produces valid SARIF fragment' { $r = New-BsiComplianceResult -ControlId 'ARCH.5.2' -Title 'NSG Deny' ` -Category 'Network' -Mode ([BsiCheckMode]::Remote) ` -Status ([BsiCheckStatus]::Fail) -Severity ([BsiSeverity]::High) ` -Details 'Missing deny rule' $r.BsiReference = 'https://bsi.bund.de' $sarif = $r.ToSarifResult() $sarif | Should -Match '"ruleId": "ARCH\.5\.2"' $sarif | Should -Match '"level": "error"' $sarif | Should -Match 'Missing deny rule' } It 'ToSarifResult returns level none for Pass' { $r = New-BsiComplianceResult -ControlId 'ARCH.5.2' -Title 'NSG Deny' ` -Category 'Network' -Mode ([BsiCheckMode]::Remote) ` -Status ([BsiCheckStatus]::Pass) -Severity ([BsiSeverity]::High) ` -Details 'All good' $sarif = $r.ToSarifResult() $sarif | Should -Match '"level": "none"' } It 'ToJUnitElement produces testcase for pass' { $r = New-BsiComplianceResult -ControlId 'NOT.4' -Title 'Backup' ` -Category 'Backup' -Mode ([BsiCheckMode]::Remote) ` -Status ([BsiCheckStatus]::Pass) -Severity ([BsiSeverity]::High) ` -Details 'Protected' $xml = $r.ToJUnitElement() $xml | Should -Match '<testcase' $xml | Should -Not -Match '<failure' } It 'ToJUnitElement produces failure for fail' { $r = New-BsiComplianceResult -ControlId 'NOT.4' -Title 'Backup' ` -Category 'Backup' -Mode ([BsiCheckMode]::Remote) ` -Status ([BsiCheckStatus]::Fail) -Severity ([BsiSeverity]::High) ` -Details 'Not protected' $xml = $r.ToJUnitElement() $xml | Should -Match '<failure' $xml | Should -Match 'BSI control NOT\.4 failed' } } # ============================================================ # RESULT STORE # ============================================================ Describe 'Result store functions' { BeforeEach { Reset-BsiResults } It 'Reset-BsiResults clears all counters' { Add-BsiResult -ControlId 'TEST.1' -Title 'Test' -Category 'Unit' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Pass) ` -Severity ([BsiSeverity]::Low) -Details 'dummy' Reset-BsiResults $summary = Get-BsiSummary $summary.Total | Should -Be 0 $summary.Passed | Should -Be 0 } It 'Add-BsiResult increments pass counter' { Add-BsiResult -ControlId 'TEST.1' -Title 'Test' -Category 'Unit' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Pass) ` -Severity ([BsiSeverity]::Low) -Details 'pass test' $summary = Get-BsiSummary $summary.Passed | Should -Be 1 $summary.Total | Should -Be 1 } It 'Add-BsiResult increments fail counter' { Add-BsiResult -ControlId 'TEST.2' -Title 'Test' -Category 'Unit' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Fail) ` -Severity ([BsiSeverity]::Medium) -Details 'fail test' $summary = Get-BsiSummary $summary.Failed | Should -Be 1 } It 'Add-BsiResult increments skip counter' { Add-BsiResult -ControlId 'TEST.3' -Title 'Test' -Category 'Unit' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Skip) ` -Severity ([BsiSeverity]::Low) -Details 'skip test' $summary = Get-BsiSummary $summary.Skipped | Should -Be 1 } It 'Add-BsiResultObject adds ComplianceResult to store' { $r = New-BsiComplianceResult -ControlId 'OBJ.1' -Title 'Object Test' ` -Category 'Unit' -Mode ([BsiCheckMode]::Remote) ` -Status ([BsiCheckStatus]::Pass) -Severity ([BsiSeverity]::Low) ` -Details 'via object' Add-BsiResultObject -Result $r $summary = Get-BsiSummary $summary.Total | Should -Be 1 $summary.Passed | Should -Be 1 } It 'Get-BsiSummary calculates score correctly' { Add-BsiResult -ControlId 'S1' -Title 'T' -Category 'C' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Pass) ` -Severity ([BsiSeverity]::Low) -Details 'p1' Add-BsiResult -ControlId 'S2' -Title 'T' -Category 'C' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Pass) ` -Severity ([BsiSeverity]::Low) -Details 'p2' Add-BsiResult -ControlId 'S3' -Title 'T' -Category 'C' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Fail) ` -Severity ([BsiSeverity]::Medium) -Details 'f1' $summary = Get-BsiSummary $summary.Score | Should -Be 66.7 } } # ============================================================ # CATALOG & MAPPING # ============================================================ Describe 'Catalog functions' { It 'Test-BsiCatalogExists returns false for missing path' { $result = $false try { $result = Test-BsiCatalogExists -Path (Join-Path $TestDrive 'nonexistent' 'catalog.json') } catch { # Private function — skip gracefully Set-ItResult -Skipped -Because 'Test-BsiCatalogExists is a private function (not exported)' return } $result | Should -BeFalse } It 'Get-BsiCatalog returns null for missing path' { $result = $null try { $result = Get-BsiCatalog -Path (Join-Path $TestDrive 'nonexistent' 'catalog.json') } catch { Set-ItResult -Skipped -Because 'Get-BsiCatalog is a private function (not exported)' return } $result | Should -BeNullOrEmpty } } Describe 'Mapping functions' { $mapPath = Join-Path $moduleRoot 'Data' 'bsi-azure-mapping.json' It 'Get-BsiMapping loads valid mapping' { if (Test-Path -LiteralPath $mapPath) { $m = Get-BsiMapping -Path $mapPath $m | Should -Not -BeNullOrEmpty $m.mappings | Should -Not -BeNullOrEmpty } else { Set-ItResult -Skipped -Because "mapping file not found at $mapPath" } } } # ============================================================ # CHECK MANIFEST # ============================================================ Describe 'Check manifest' { $manifestPath = Join-Path $moduleRoot 'Checks' '_CheckManifest.json' It 'exists' { if (-not $manifestPath) { Set-ItResult -Skipped -Because 'moduleRoot not resolved' return } Test-Path -LiteralPath $manifestPath | Should -BeTrue } It 'is valid JSON' { if (-not $manifestPath -or -not (Test-Path -LiteralPath $manifestPath)) { Set-ItResult -Skipped -Because 'manifest not found' return } $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json $manifest | Should -Not -BeNullOrEmpty $manifest.checks.Count | Should -BeGreaterThan 0 } It 'all declared check functions have matching .ps1 files' { if (-not $manifestPath -or -not (Test-Path -LiteralPath $manifestPath)) { Set-ItResult -Skipped -Because 'manifest not found' return } $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json foreach ($check in $manifest.checks) { $checkPath = Join-Path $moduleRoot $check.file Test-Path -LiteralPath $checkPath | Should -BeTrue -Because "check $($check.id) needs $($check.file)" } } It 'every check has a unique id' { if (-not $manifestPath -or -not (Test-Path -LiteralPath $manifestPath)) { Set-ItResult -Skipped -Because 'manifest not found' return } $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json $ids = $manifest.checks | ForEach-Object { $_.id } $unique = $ids | Sort-Object -Unique $ids.Count | Should -Be $unique.Count } } # ============================================================ # AZ CLI WRAPPER # ============================================================ Describe 'Get-AzCliResponse' { It 'Test-AzCliReady succeeds when az is available' { try { Test-AzCliReady | Out-Null $true | Should -BeTrue } catch { Set-ItResult -Skipped -Because 'az CLI not available in test environment' } } } # ============================================================ # EXPORTER FUNCTIONS # ============================================================ Describe 'Exporters' { BeforeEach { Reset-BsiResults Add-BsiResult -ControlId 'EXP.1' -Title 'Export Test' -Category 'Unit' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Pass) ` -Severity ([BsiSeverity]::Low) -Details 'export pass' Add-BsiResult -ControlId 'EXP.2' -Title 'Export Test 2' -Category 'Unit' ` -Mode ([BsiCheckMode]::Local) -Status ([BsiCheckStatus]::Fail) ` -Severity ([BsiSeverity]::Medium) -Details 'export fail' } It 'Export-Console returns output without error' { $output = Export-Console -Results @((Get-BsiSummary).Results) $output | Should -Not -BeNullOrEmpty } It 'Export-Json writes valid JSON' { $tempFile = Join-Path $TestDrive 'test-export.json' Export-Json -Results @((Get-BsiSummary).Results) -OutputPath $tempFile Test-Path -LiteralPath $tempFile | Should -BeTrue $json = Get-Content -Raw -LiteralPath $tempFile | ConvertFrom-Json $json.summary | Should -Not -BeNullOrEmpty $json.controls.Count | Should -Be 2 } It 'Export-Sarif writes valid SARIF 2.1.0' { $tempFile = Join-Path $TestDrive 'test-export.sarif' Export-Sarif -Results @((Get-BsiSummary).Results) -OutputPath $tempFile $sarif = Get-Content -Raw -LiteralPath $tempFile | ConvertFrom-Json $sarif.version | Should -Be '2.1.0' $sarif.runs[0].results.Count | Should -Be 2 } It 'Export-JUnitXml writes valid XML' { $tempFile = Join-Path $TestDrive 'test-export.junit.xml' Export-JUnitXml -Results @((Get-BsiSummary).Results) -OutputPath $tempFile [xml]$xml = Get-Content -Raw -LiteralPath $tempFile $xml.testsuites.testsuite.count | Should -Be 1 } It 'Export-Html writes HTML file' { $tempFile = Join-Path $TestDrive 'test-export.html' Export-Html -Results @((Get-BsiSummary).Results) -OutputPath $tempFile Test-Path -LiteralPath $tempFile | Should -BeTrue $html = Get-Content -Raw -LiteralPath $tempFile $html | Should -Match '<!DOCTYPE html>' $html | Should -Match 'BSI IT-Grundschutz' } } |