Private/PathHelpers.ps1

function Test-IsAdmin {
    <#
    .SYNOPSIS
        Returns $true if running as Administrator, $false otherwise.
    #>

    [CmdletBinding()]
    param()
    
    return ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-PathEntries {
    <#
    .SYNOPSIS
        Retrieves all PATH entries for both User and Machine scopes.
    #>

    [CmdletBinding()]
    param(
        [ValidateSet('User', 'Machine', 'Both')]
        [string]$Target = 'Both'
    )

    $result = @{
        User    = @()
        Machine = @()
    }

    if ($Target -eq 'User' -or $Target -eq 'Both') {
        $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
        if ($userPath) {
            $result.User = $userPath -split ';' | Where-Object { $_ -ne '' }
        }
    }

    if ($Target -eq 'Machine' -or $Target -eq 'Both') {
        $machinePath = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
        if ($machinePath) {
            $result.Machine = $machinePath -split ';' | Where-Object { $_ -ne '' }
        }
    }

    return $result
}

function Test-PathEntry {
    <#
    .SYNOPSIS
        Tests if a PATH entry exists and is valid.
    #>

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

    # Expand environment variables
    $expandedPath = [Environment]::ExpandEnvironmentVariables($Path)
    
    $exists = $false
    try {
        $exists = Test-Path -LiteralPath $expandedPath -PathType Container -ErrorAction Stop
    }
    catch {
        # Access denied or other errors - treat as inaccessible
        $exists = $false
    }
    
    return @{
        Original = $Path
        Expanded = $expandedPath
        Exists   = $exists
    }
}

function Find-DuplicatePaths {
    <#
    .SYNOPSIS
        Finds duplicate PATH entries (case-insensitive).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Paths
    )

    $seen = @{}
    $duplicates = @()

    foreach ($path in $Paths) {
        $normalized = $path.TrimEnd('\').ToLowerInvariant()
        $expanded = [Environment]::ExpandEnvironmentVariables($normalized)
        
        if ($seen.ContainsKey($expanded)) {
            $duplicates += @{
                Path        = $path
                DuplicateOf = $seen[$expanded]
            }
        }
        else {
            $seen[$expanded] = $path
        }
    }

    return $duplicates
}

function Get-PathCharacterCount {
    <#
    .SYNOPSIS
        Gets the total character count of a PATH string.
    #>

    [CmdletBinding()]
    param(
        [string[]]$Paths
    )

    if ($null -eq $Paths -or $Paths.Count -eq 0) {
        return 0
    }

    return ($Paths -join ';').Length
}

function Test-PathSecurity {
    <#
    .SYNOPSIS
        Validates a path for security issues.
    .DESCRIPTION
        Checks for:
        - Forbidden characters in Windows paths (< > " | ? *)
        - Reserved Windows device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
        - Path traversal attacks (..\ or ../)
        - Control characters (ASCII 0-31)
        - Null bytes (injection attacks)
        - Excessively long paths
        - UNC paths to potentially dangerous locations
    .PARAMETER Path
        The path to validate.
    .OUTPUTS
        Returns a hashtable with:
        - IsValid: $true if path is safe, $false otherwise
        - Issues: Array of security issues found
        - Severity: 'Safe', 'Warning', or 'Critical'
    .EXAMPLE
        Test-PathSecurity -Path "C:\Windows\System32"
        # Returns: @{ IsValid = $true; Issues = @(); Severity = 'Safe' }
    .EXAMPLE
        Test-PathSecurity -Path "C:\Users\..\Windows"
        # Returns: @{ IsValid = $false; Issues = @('Path traversal detected'); Severity = 'Critical' }
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Path
    )

    $issues = @()
    $severity = 'Safe'

    # Check for empty or null path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        return @{
            IsValid  = $false
            Issues   = @('Path is empty or whitespace only')
            Severity = 'Critical'
        }
    }

    # Check for null bytes (injection attack vector)
    if ($Path.Contains([char]0)) {
        $issues += 'Null byte detected (potential injection attack)'
        $severity = 'Critical'
    }

    # Check for control characters (ASCII 0-31, except tab which is sometimes used)
    $controlChars = [regex]::Matches($Path, '[\x00-\x08\x0B\x0C\x0E-\x1F]')
    if ($controlChars.Count -gt 0) {
        $issues += "Control characters detected (ASCII codes: $($controlChars | ForEach-Object { [int][char]$_.Value } | Sort-Object -Unique))"
        $severity = 'Critical'
    }

    # Check for forbidden characters in Windows paths
    # Note: We allow : for drive letters (C:) and \ / for path separators
    # These characters are CRITICAL because they cannot exist in valid Windows paths
    $forbiddenChars = '<', '>', '"', '|', '?', '*'
    foreach ($char in $forbiddenChars) {
        if ($Path.Contains($char)) {
            $issues += "Forbidden character '$char' in path"
            $severity = 'Critical'
        }
    }

    # Check for reserved Windows device names
    # These cannot be used as file or folder names
    $reservedNames = @(
        'CON', 'PRN', 'AUX', 'NUL',
        'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
        'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
    )
    
    # Split path and check each component
    $pathComponents = $Path -split '[\\\/]' | Where-Object { $_ -ne '' }
    foreach ($component in $pathComponents) {
        # Remove extension for comparison (CON.txt is also reserved)
        $nameWithoutExt = $component -replace '\.[^.]*$', ''
        
        if ($reservedNames -contains $nameWithoutExt.ToUpperInvariant()) {
            $issues += "Reserved Windows name '$component' in path"
            $severity = 'Critical'
        }
    }

    # Check for path traversal attacks
    # Look for patterns like ..\ or ../ that could escape intended directory
    if ($Path -match '\.\.[\\\/]' -or $Path -match '[\\\/]\.\.([\\\/]|$)') {
        $issues += 'Path traversal detected (..\ or ../ pattern)'
        $severity = 'Critical'
    }

    # Check for paths starting with .. (relative path traversal)
    if ($Path -match '^\.\.') {
        $issues += 'Path starts with parent directory reference (..)'
        if ($severity -ne 'Critical') { $severity = 'Warning' }
    }

    # Check for excessively long paths (Windows MAX_PATH is 260, but long paths can be 32767)
    if ($Path.Length -gt 32767) {
        $issues += "Path exceeds maximum length (32767 characters)"
        $severity = 'Critical'
    }
    elseif ($Path.Length -gt 260) {
        $issues += "Path exceeds legacy MAX_PATH (260 characters) - may not work on older systems"
        if ($severity -eq 'Safe') { $severity = 'Warning' }
    }

    # Check for suspicious UNC paths
    if ($Path -match '^\\\\') {
        # UNC path - add warning but don't block
        $issues += 'UNC network path detected - verify this is a trusted location'
        if ($severity -eq 'Safe') { $severity = 'Warning' }
        
        # Check for UNC paths to localhost with traversal
        if ($Path -match '^\\\\(localhost|127\.0\.0\.1|::1)\\.*\.\.') {
            $issues += 'Suspicious UNC path with traversal to localhost'
            $severity = 'Critical'
        }
    }

    # Check for paths with trailing dots or spaces (Windows normalizes these, can be confusing)
    if ($Path -match '\s+$' -or $Path -match '\.+$') {
        $issues += 'Path ends with spaces or dots (Windows will normalize these)'
        if ($severity -eq 'Safe') { $severity = 'Warning' }
    }

    # Check for multiple consecutive separators (could indicate path manipulation)
    if ($Path -match '[\\\/]{3,}') {
        $issues += 'Multiple consecutive path separators detected'
        if ($severity -eq 'Safe') { $severity = 'Warning' }
    }

    # Check for mixed path separators (potential confusion attack)
    if ($Path -match '\\' -and $Path -match '/') {
        $issues += 'Mixed path separators (\ and /) detected'
        if ($severity -eq 'Safe') { $severity = 'Warning' }
    }

    return @{
        IsValid  = ($severity -ne 'Critical')
        Issues   = $issues
        Severity = $severity
    }
}