Private/Security/Test-PathTraversal.ps1

function Test-PathTraversal {
    <#
    .SYNOPSIS
        Tests for path traversal vulnerabilities in file paths
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$Path,

        [Parameter()]
        [string]$BasePath = (Get-Location).Path,

        [Parameter()]
        [string]$CorrelationId = [System.Guid]::NewGuid().ToString()
    )

    begin {
        $validationResults = @()
        $totalPaths = 0
        $safePaths = 0
        $unsafePaths = 0

        # Common traversal patterns
        $traversalPatterns = @(
            '\.\.[\\/]',
            '\.\.%2f',
            '\.\.%5c',
            '%2e%2e%2f',
            '%2e%2e%5c'
        )
    }

    process {
        # Validate base path first - throw error for invalid base paths
        try {
            $resolvedBasePath = Resolve-Path $BasePath -ErrorAction Stop
        } catch {
            throw "Invalid base path: $BasePath - $($_.Exception.Message)"
        }

        foreach ($currentPath in $Path) {
            $totalPaths++

            try {
                # Check for traversal patterns first (before path existence)
                $patternFound = $false
                foreach ($pattern in $traversalPatterns) {
                    if ($currentPath -match $pattern) {
                        $patternFound = $true
                        break
                    }
                }

                if (Test-Path $currentPath) {
                    $resolvedPath = Resolve-Path $currentPath -ErrorAction Stop
                    
                    # Check if this is a symbolic link or junction and resolve to target
                    $finalTargetPath = $resolvedPath.Path
                    $isSymbolicLink = $false
                    
                    try {
                        $item = Get-Item $currentPath -ErrorAction Stop
                        if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
                            $isSymbolicLink = $true
                            # For symbolic links/junctions, get the actual target
                            if ($item.Target) {
                                $targetPath = $item.Target[0]  # Get first target
                                if ([System.IO.Path]::IsPathRooted($targetPath)) {
                                    $finalTargetPath = $targetPath
                                } else {
                                    # Relative target - resolve relative to link directory
                                    $linkDirectory = Split-Path $resolvedPath.Path -Parent
                                    $finalTargetPath = Join-Path $linkDirectory $targetPath
                                    $finalTargetPath = [System.IO.Path]::GetFullPath($finalTargetPath)
                                }
                            }
                        }
                    }
                    catch {
                        # If we can't determine symbolic link status, continue with original path
                        Write-Verbose "Could not determine symbolic link status for $currentPath : $($_.Exception.Message)"
                    }

                    # Check if final target path is within base path
                    $isWithinBasePath = $finalTargetPath.StartsWith($resolvedBasePath.Path, [System.StringComparison]::OrdinalIgnoreCase)

                    $isSafe = $isWithinBasePath -and (-not $patternFound)

                    if ($isSafe) {
                        $safePaths++
                    } else {
                        $unsafePaths++
                    }

                    $validationResults += [PSCustomObject]@{
                        OriginalPath = $currentPath
                        ResolvedPath = $resolvedPath.Path
                        FinalTargetPath = $finalTargetPath
                        BasePath = $resolvedBasePath.Path
                        IsSymbolicLink = $isSymbolicLink
                        IsWithinBasePath = $isWithinBasePath
                        ContainsTraversalPattern = $patternFound
                        IsSafe = $isSafe
                        CorrelationId = $CorrelationId
                    }
                } else {
                    # Path doesn't exist, but still check for traversal patterns
                    $unsafePaths++
                    
                    # Only warn in non-test environments to reduce test noise
                    if (-not $env:PESTER_TESTING) {
                        Write-Warning "Path does not exist: $currentPath"
                    }
                    
                    $validationResults += [PSCustomObject]@{
                        OriginalPath = $currentPath
                        ResolvedPath = $null
                        BasePath = $BasePath
                        IsWithinBasePath = $false
                        ContainsTraversalPattern = $patternFound
                        IsSafe = $false
                        CorrelationId = $CorrelationId
                    }
                }
            } catch {
                $unsafePaths++
                Write-Error "Error validating path $currentPath : $($_.Exception.Message)"
                
                $validationResults += [PSCustomObject]@{
                    OriginalPath = $currentPath
                    ResolvedPath = $null
                    BasePath = $BasePath
                    IsWithinBasePath = $false
                    ContainsTraversalPattern = $patternFound
                    IsSafe = $false
                    CorrelationId = $CorrelationId
                }
            }
        }
    }

    end {
        $validationPassed = ($unsafePaths -eq 0)

        $validationSummary = @{
            TotalPaths = $totalPaths
            SafePaths = $safePaths
            UnsafePaths = $unsafePaths
            ValidationPassed = $validationPassed
        }

        # Log completion summary
        if (Get-Command Write-StructuredLog -ErrorAction SilentlyContinue) {
            $logLevel = if (-not $validationPassed) { 'Warning' } else { 'Information' }
            Write-StructuredLog -Level $logLevel -Message "Path traversal validation completed" -CorrelationId $CorrelationId -Data $validationSummary
        }

        return [PSCustomObject]@{
            PSTypeName = 'PathTraversalValidationResult'
            ValidationResults = $validationResults
            TotalPaths = $totalPaths
            SafePaths = $safePaths
            UnsafePaths = $unsafePaths
            ValidationPassed = $validationPassed
            BasePath = $BasePath
            CorrelationId = $CorrelationId
        }
    }
}