tests/Security.Module.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } #Requires -Modules ImportExcel <# .SYNOPSIS Pester tests for all Security inventory modules. .DESCRIPTION Tests both Processing and Reporting phases for Security modules. Defender modules (DefenderAlerts, DefenderAssessments, DefenderPricing, DefenderSecureScore) call live Az Security cmdlets in Processing — those are verified for graceful failure and empty-data Reporting behavior. Key Vault (Vault.ps1) uses $Resources and is fully tested with mock data. .NOTES Author: AzureScout Contributors Version: 1.0.0 Created: 2026-02-24 Phase: 9.2 — Defender for Cloud Testing #> # =================================================================== # DISCOVERY-TIME # =================================================================== $SecurityPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Modules' 'Public' 'InventoryModules' 'Security' $SecurityModuleFiles = @( @{ Name = 'Vault'; File = 'Vault.ps1' } @{ Name = 'DefenderAlerts'; File = 'DefenderAlerts.ps1' } @{ Name = 'DefenderAssessments'; File = 'DefenderAssessments.ps1' } @{ Name = 'DefenderPricing'; File = 'DefenderPricing.ps1' } @{ Name = 'DefenderSecureScore'; File = 'DefenderSecureScore.ps1' } ) # =================================================================== # EXECUTION-TIME SETUP # =================================================================== BeforeAll { $script:ModuleRoot = Split-Path -Parent $PSScriptRoot $script:SecurityPath = Join-Path $script:ModuleRoot 'Modules' 'Public' 'InventoryModules' 'Security' $script:TempDir = Join-Path $env:TEMP 'AZSC_SecurityTests' if (Test-Path $script:TempDir) { Remove-Item $script:TempDir -Recurse -Force } New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null function New-MockSecResource { param([string]$Id, [string]$Name, [string]$Type, [string]$Kind = '', [string]$Location = 'eastus', [string]$RG = 'rg-sec', [string]$SubscriptionId = 'sub-00000001', [object]$Props) [PSCustomObject]@{ id = $Id NAME = $Name TYPE = $Type KIND = $Kind LOCATION = $Location RESOURCEGROUP = $RG subscriptionId = $SubscriptionId tags = [PSCustomObject]@{} PROPERTIES = $Props } } $script:MockResources = @() # Key Vault $script:MockResources += New-MockSecResource -Id '/sec/kv1' -Name 'kv-prod' ` -Type 'microsoft.keyvault/vaults' -Props ([PSCustomObject]@{ sku = [PSCustomObject]@{ name = 'premium'; family = 'A' } tenantId = 'tenant-001' enabledForDeployment = $false enabledForTemplateDeployment = $true enabledForDiskEncryption = $true enableSoftDelete = $true softDeleteRetentionInDays = 90 enablePurgeProtection = $true enableRbacAuthorization = $true publicNetworkAccess = 'Disabled' networkAcls = [PSCustomObject]@{ bypass = 'AzureServices'; defaultAction = 'Deny'; ipRules = @(); virtualNetworkRules = @() } provisioningState = 'Succeeded' vaultUri = 'https://kv-prod.vault.azure.net/' }) } AfterAll { if (Test-Path $script:TempDir) { Remove-Item $script:TempDir -Recurse -Force } } # =================================================================== # TESTS # =================================================================== Describe 'Security Module Files Exist' { It 'Security module folder exists' { $script:SecurityPath | Should -Exist } It '<Name> module file exists' -ForEach $SecurityModuleFiles { Join-Path $script:SecurityPath $File | Should -Exist } } Describe 'Key Vault (Vault.ps1) — Processing' { It 'Processing returns results for Key Vault resources' { $modFile = Join-Path $script:SecurityPath 'Vault.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) $result = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null $result | Should -Not -BeNullOrEmpty } It 'Processing returns no output for empty resource list without throwing' { $modFile = Join-Path $script:SecurityPath 'Vault.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, @(), $null, 'Processing', $null, $null, 'Light20', $null } | Should -Not -Throw } It 'Processing output contains expected Key Vault fields' { $modFile = Join-Path $script:SecurityPath 'Vault.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) $result = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null $row = $result | Select-Object -First 1 $row.Keys | Should -Contain 'Name' $row.Keys | Should -Contain 'Subscription' $row.Keys | Should -Contain 'Resource Group' } } Describe 'Key Vault (Vault.ps1) — Reporting' { BeforeAll { $modFile = Join-Path $script:SecurityPath 'Vault.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) $script:KvProcessed = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null $script:KvXlsx = Join-Path $script:TempDir 'Vault_test.xlsx' } It 'Reporting phase does not throw' { $modFile = Join-Path $script:SecurityPath 'Vault.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $null, $null, 'Reporting', $script:KvXlsx, $script:KvProcessed, 'Light20', $null } | Should -Not -Throw } It 'Excel file is created' { $script:KvXlsx | Should -Exist } } Describe 'Defender Modules — Graceful Behavior Without Live Azure Connection' { It 'DefenderAlerts Processing does not throw when Get-AzSecurityAlert is unavailable' { $modFile = Join-Path $script:SecurityPath 'DefenderAlerts.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, @(), $null, 'Processing', $null, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderAssessments Processing does not throw when Get-AzSecurityAssessment is unavailable' { $modFile = Join-Path $script:SecurityPath 'DefenderAssessments.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, @(), $null, 'Processing', $null, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderPricing Processing does not throw when Get-AzSecurityPricing is unavailable' { $modFile = Join-Path $script:SecurityPath 'DefenderPricing.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, @(), $null, 'Processing', $null, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderSecureScore Processing does not throw when Get-AzSecuritySecureScore is unavailable' { $modFile = Join-Path $script:SecurityPath 'DefenderSecureScore.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, @(), $null, 'Processing', $null, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderAlerts Reporting does not throw with empty data' { $modFile = Join-Path $script:SecurityPath 'DefenderAlerts.ps1' $content = Get-Content -Path $modFile -Raw $xlsxFile = Join-Path $script:TempDir 'DefenderAlerts_empty.xlsx' $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $null, $null, 'Reporting', $xlsxFile, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderAssessments Reporting does not throw with empty data' { $modFile = Join-Path $script:SecurityPath 'DefenderAssessments.ps1' $content = Get-Content -Path $modFile -Raw $xlsxFile = Join-Path $script:TempDir 'DefenderAssessments_empty.xlsx' $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $null, $null, 'Reporting', $xlsxFile, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderPricing Reporting does not throw with empty data' { $modFile = Join-Path $script:SecurityPath 'DefenderPricing.ps1' $content = Get-Content -Path $modFile -Raw $xlsxFile = Join-Path $script:TempDir 'DefenderPricing_empty.xlsx' $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $null, $null, 'Reporting', $xlsxFile, $null, 'Light20', $null } | Should -Not -Throw } It 'DefenderSecureScore Reporting does not throw with empty data' { $modFile = Join-Path $script:SecurityPath 'DefenderSecureScore.ps1' $content = Get-Content -Path $modFile -Raw $xlsxFile = Join-Path $script:TempDir 'DefenderSecureScore_empty.xlsx' $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $null, $null, 'Reporting', $xlsxFile, $null, 'Light20', $null } | Should -Not -Throw } } Describe 'Defender modules contain correct Azure resource type filters' { It 'DefenderAlerts.ps1 contains Get-AzSecurityAlert call' { $modFile = Join-Path $script:SecurityPath 'DefenderAlerts.ps1' $content = Get-Content -Path $modFile -Raw $content | Should -Match 'Get-AzSecurityAlert' } It 'DefenderAssessments.ps1 contains Get-AzSecurityAssessment call' { $modFile = Join-Path $script:SecurityPath 'DefenderAssessments.ps1' $content = Get-Content -Path $modFile -Raw $content | Should -Match 'Get-AzSecurityAssessment' } It 'DefenderPricing.ps1 contains Get-AzSecurityPricing call' { $modFile = Join-Path $script:SecurityPath 'DefenderPricing.ps1' $content = Get-Content -Path $modFile -Raw $content | Should -Match 'Get-AzSecurityPricing' } It 'DefenderSecureScore.ps1 contains Get-AzSecuritySecureScore call' { $modFile = Join-Path $script:SecurityPath 'DefenderSecureScore.ps1' $content = Get-Content -Path $modFile -Raw $content | Should -Match 'Get-AzSecuritySecureScore' } } |