Testing Security Functions with Pester: A Real-World Example

When building security-focused PowerShell functions, comprehensive testing isn't just good practice—it's essential. Today, I'll walk you through a real-world example of how to use Pester to thoroughly test a path traversal validation function, demonstrating testing patterns that can be applied to any PowerShell security module.

The files this blog references are Test-PathTraversal.ps1 and its companion test Test-PathTraversal.tests.ps1 located on my GitHub.

Pester Version Context

This example uses Pester 3.4, which is installed by default on Windows systems and remains widely used in enterprise environments. While Pester has evolved significantly with newer versions (currently at 5.x), understanding 3.4 syntax is valuable because:

  • It's the default version on Windows Server and many corporate workstations
  • Many existing test suites use this syntax
  • Migration paths exist to newer versions when ready

Key differences you'll notice in this Pester 3.4 example:

  • Uses Should Be instead of the newer Should -Be syntax
  • Relies on script-scoped variables ($script:) for test data sharing
  • Uses the older BeforeAll/AfterAll structure

Benefits of upgrading to Pester 5.x include:

  • Improved performance and parallel test execution
  • Better error reporting and debugging capabilities
  • More flexible configuration options
  • Enhanced mocking capabilities
  • Cleaner syntax with pipeline-based assertions

The Function Under Test

Our example centers around Test-PathTraversal, a PowerShell function designed to detect and prevent path traversal attacks—a common security vulnerability where malicious users attempt to access files outside of intended directories using patterns like ../../../etc/passwd.

The function validates file paths against several criteria:

  • Detects common traversal patterns (..\, %2e%2e%2f, etc.)
  • Ensures paths remain within a specified base directory
  • Handles both existing and non-existing paths
  • Provides detailed validation results with correlation tracking

Test Structure and Organization

The Pester test suite is organized into logical contexts that mirror real-world usage scenarios:

Describe "Test-PathTraversal" {
    BeforeAll {
        # Set up test environment
        $env:PESTER_TESTING = $true
        
        # Create isolated test directory structure
        $script:TestBasePath = Join-Path $env:TEMP "PathTraversalTests"
        # ... setup test files and directories
    }

    Context "Parameter Validation" { }
    Context "Path Traversal Detection" { }
    Context "Security Validation" { }
    Context "Error Handling" { }
    Context "Batch Processing" { }
    Context "Pipeline Support" { }
    Context "Logging Integration" { }
    Context "Result Object Structure" { }
    Context "Performance Requirements" { }
}

This structure provides clear separation of concerns and makes it easy to understand what aspects of the function are being tested.

Key Testing Patterns

1. Isolated Test Environment

The tests create a completely isolated test environment in the temp directory:

BeforeAll {
    $script:TestBasePath = Join-Path $env:TEMP "PathTraversalTests"
    if (Test-Path $script:TestBasePath) {
        Remove-Item $script:TestBasePath -Recurse -Force
    }
    New-Item -Path $script:TestBasePath -ItemType Directory -Force | Out-Null
    
    # Create nested directories and test files
    $script:SafeDir = Join-Path $script:TestBasePath "SafeDirectory"
    $script:SafeFile = Join-Path $script:SafeDir "safe.txt"
    # ...
}

This ensures tests are completely independent and don't interfere with the actual file system.

2. Security-Focused Test Cases

The tests include comprehensive security validation scenarios:

It "Should reject common traversal attack patterns" {
    $attackPatterns = @(
        "..\..\..\etc\passwd",
        "..\..\Windows\System32\config",
        "..\..\..\var\log\auth.log",
        "..%2f..%2f..%2fetc%2fpasswd"
    )
    
    foreach ($pattern in $attackPatterns) {
        $testPath = Join-Path $script:SafeDir $pattern
        $result = Test-PathTraversal -Path $testPath -BasePath $script:TestBasePath
        $result.ValidationResults[0].ContainsTraversalPattern | Should Be $true
    }
}

This pattern ensures the function can detect various attack vectors, including URL-encoded attempts.

3. Pipeline Testing

PowerShell's pipeline capabilities are thoroughly tested:

It "Should support pipeline input from Get-ChildItem" {
    $pipelineResult = Get-ChildItem $script:SafeDir -Recurse | 
        Select-Object -ExpandProperty FullName | 
        Test-PathTraversal -BasePath $script:TestBasePath
    
    $pipelineResult.TotalPaths | Should BeGreaterThan 0
    $pipelineResult.SafePaths | Should Be $pipelineResult.TotalPaths
    $pipelineResult.ValidationPassed | Should Be $true
}

This ensures the function works seamlessly with PowerShell's object pipeline.

4. Performance Testing

Critical for security functions that might process large datasets:

It "Should complete within acceptable time limits" {
    $largeBatch = 1..100 | ForEach-Object { $script:SafeFile }
    
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    $result = Test-PathTraversal -Path $largeBatch -BasePath $script:TestBasePath
    $stopwatch.Stop()
    
    $stopwatch.ElapsedMilliseconds | Should BeLessThan 10000  # 10 seconds max
    $result.TotalPaths | Should Be 100
}

Performance tests help ensure the function remains responsive under load.

5. Error Handling Validation

Robust error handling is crucial for security functions:

It "Should handle invalid base path gracefully" {
    $invalidBasePath = "Z:\NonExistent\Path"
    { Test-PathTraversal -Path $script:SafeFile -BasePath $invalidBasePath } | Should Throw
}

It "Should handle non-existent paths gracefully" {
    $nonExistentPath = Join-Path $script:TestBasePath "nonexistent.txt"
    $result = Test-PathTraversal -Path $nonExistentPath -BasePath $script:TestBasePath
    $result.UnsafePaths | Should Be 1
    $result.ValidationPassed | Should Be $false
}

These tests ensure the function fails safely and provides meaningful feedback.

Advanced Testing Techniques

Mocking External Dependencies

The tests mock external logging functions to focus on the core functionality:

BeforeAll {
    Mock Write-StructuredLog { }
}

This isolates the function under test from external dependencies.

Environment-Aware Testing

The tests set environment variables to modify behavior during testing:

BeforeAll {
    $env:PESTER_TESTING = $true
}

AfterAll {
    Remove-Item -Path "env:PESTER_TESTING" -ErrorAction SilentlyContinue
}

This allows the function to suppress warnings during testing while maintaining them in production.

Comprehensive Result Validation

Tests validate not just the final result, but the structure and completeness of returned data:

It "Should return properly structured result object" {
    $result = Test-PathTraversal -Path $script:SafeFile -BasePath $script:TestBasePath
    
    $result | Should Not BeNullOrEmpty
    $result.PSObject.TypeNames[0] | Should Be 'PathTraversalValidationResult'
    $result.ValidationResults | Should Not BeNullOrEmpty
    $result.TotalPaths | Should BeGreaterThan 0
}

This ensures the function contract is maintained across changes.

Best Practices Demonstrated

  1. Complete Test Coverage: Every public parameter and code path is tested
  2. Security-First Approach: Attack scenarios are explicitly tested
  3. Performance Considerations: Load and memory usage are validated
  4. Real-World Scenarios: Pipeline usage and batch processing are covered
  5. Clean Setup/Teardown: Tests are completely isolated and repeatable
  6. Meaningful Assertions: Tests validate both positive and negative cases
  7. Error Scenario Coverage: Invalid inputs and edge cases are handled

Running the Tests

With Pester 3.4 (Default on Windows)

These tests are designed for Pester 3.4, which comes pre-installed on Windows systems:

# Check your Pester version
Get-Module -Name Pester -ListAvailable

# Run all tests (Pester 3.4 syntax)
Invoke-Pester -Script ".\Test-PathTraversal.Tests.ps1" -Verbose

# Run with detailed output
Invoke-Pester -Script ".\Test-PathTraversal.Tests.ps1" -OutputFormat NUnitXml -OutputFile "TestResults.xml"

When executed, you'll see Pester work through each test context systematically, validating everything from basic parameter handling to complex security scenarios:

The output shows each test context being executed (Parameter Validation, Path Traversal Detection, Security Validation, etc.) with individual test results. Notice how Pester provides clear feedback on which tests pass and gives you a comprehensive summary at the end showing total tests run, passed, failed, and execution time.

Upgrading to Pester 5.x

If you want to upgrade to the latest Pester version for enhanced features:

# Remove the built-in version and install latest
Remove-Module -Name Pester -Force -ErrorAction SilentlyContinue
Install-Module -Name Pester -Force -SkipPublisherCheck

# Verify new version
Get-Module -Name Pester -ListAvailable

# Run tests with Pester 5.x (requires syntax updates)
Invoke-Pester -Path ".\Test-PathTraversal.Tests.ps1" -Output Detailed

Note: Upgrading to Pester 5.x will require updating the test syntax. The core testing logic remains the same, but assertions change from Should Be to Should -Be, and configuration becomes more structured.

Conclusion

This example demonstrates how Pester can be used to create comprehensive test suites for security-critical PowerShell functions. The key takeaways are:

  • Structure tests logically using Describe and Context blocks
  • Test security scenarios explicitly with known attack patterns
  • Validate performance characteristics for functions that process large datasets
  • Test PowerShell-specific features like pipeline support
  • Ensure complete isolation between test runs
  • Mock external dependencies to focus on core functionality

By following these patterns, you can build confidence in your PowerShell security modules and catch potential vulnerabilities before they reach production.

The comprehensive nature of these tests—covering 50+ test cases across 9 different contexts—demonstrates the level of rigor needed when testing security-focused code. This investment in testing pays dividends in reliability, maintainability, and security assurance.

Remember: when it comes to security functions, it's better to over-test than under-test. The extra effort spent creating comprehensive test suites like this one will save countless hours debugging production issues and security incidents.