Tests/Pester.PSSecRules.Tests.ps1

BeforeAll {
    function RunRuleForCommand
    {
        param([String] $Command)

        $outputPath = Join-Path $env:TEMP ([IO.Path]::GetRandomFileName() + ".ps1")
        try
        {
            Set-Content -Path $outputPath -Value $Command

            Invoke-ScriptAnalyzer -Path $outputPath `
                -CustomizedRulePath (Resolve-Path $PSScriptRoot\..\PSSecRules.psm1) `
                -ExcludeRule PS*
        }
        finally
        {
            Remove-Item $outputPath -ErrorAction SilentlyContinue
        }
    }

    function RunRuleForFile
    {
        param([String] $RelativePath)

        $filePath = Join-Path $PSScriptRoot "..\Examples\$RelativePath"
        Invoke-ScriptAnalyzer -Path $filePath `
            -CustomizedRulePath (Resolve-Path $PSScriptRoot\..\PSSecRules.psm1) `
            -ExcludeRule PS*
    }
}

Describe "Tests for PSSec.HardcodedCredential" {

    It "Should detect hardcoded password assignment" {
        $result = RunRuleForCommand {
            function Test-HardcodedCredential
            {
                $password = "sRbHG$a%"
                return $password.Length
            }
        }

        $result.RuleName | Should -Be "PSSec.HardcodedCredential"
    }

    It "Should detect hardcoded default credential parameter" {
        $result = RunRuleForCommand {
            function Test-HardcodedCredential
            {
                param([string]$ClientSecret = "my-fixed-secret")
                return $ClientSecret
            }
        }

        $result.RuleName | Should -Be "PSSec.HardcodedCredential"
    }

    It "Should detect hardcoded credential comparison" {
        $result = RunRuleForCommand {
            function Test-Login
            {
                param($userName, $pwdInput)
                if($userName -eq "admin" -and $pwdInput -eq "sRbHG$a%")
                {
                    return $true
                }
                return $false
            }
        }

        $result.RuleName | Should -Be "PSSec.HardcodedCredential"
    }
}

Describe "Tests for PSSec.SqlInjection" {

    It "Should detect dynamic SQL with string interpolation" {
        $result = RunRuleForCommand {
            function Invoke-UnsafeSql
            {
                param($userInput)
                $query = "SELECT * FROM Users WHERE UserName = '$userInput'"
                Invoke-Sqlcmd -Query $query
            }
        }

        $result.RuleName | Should -Be "PSSec.SqlInjection"
    }

    It "Should detect CommandText assignment with concatenation" {
        $result = RunRuleForCommand {
            function Invoke-UnsafeSql
            {
                param($userInput)
                $cmd = New-Object psobject
                $cmd.CommandText = "SELECT * FROM Users WHERE UserName='" + $userInput + "'"
                return $cmd
            }
        }

        $result.RuleName | Should -Be "PSSec.SqlInjection"
    }
}

Describe "Tests for PSSec.PathTraversal" {

    It "Should detect tainted variable used as Get-Content path" {
        $result = RunRuleForCommand {
            function Get-UnsafeFileContent
            {
                param($pathInput)
                Get-Content -Path $pathInput
            }
        }

        $result.RuleName | Should -Be "PSSec.PathTraversal"
    }

    It "Should detect tainted variable in .NET file API" {
        $result = RunRuleForCommand {
            function Test-EngineSchema
            {
                param($pathInput)

                if($null -ne $pathInput)
                {
                    $json = [System.IO.File]::ReadAllText($pathInput)
                    return $json
                }
            }
        }

        $result.RuleName | Should -Be "PSSec.PathTraversal"
    }
}

Describe "Tests for PSSec.Xss" {

    It "Should detect tainted URL passed to Navigate" {
        $result = RunRuleForCommand {
            function Open-HtmlPage
            {
                param($urlQuery)

                $urlRequest = "http://mysite.com?" + $urlQuery
                $browser = New-Object psobject
                $browser.Navigate($urlRequest)
            }
        }

        $result.RuleName | Should -Be "PSSec.Xss"
    }

    It "Should detect tainted comment rendered via Write" {
        $result = RunRuleForCommand {
            function Show-Comment
            {
                param($userComment)
                $response = New-Object psobject
                $response.Write($userComment)
            }
        }

        $result.RuleName | Should -Be "PSSec.Xss"
    }

    It "Should allow encoded output before sink" {
        $result = RunRuleForCommand {
            function Open-HtmlPage
            {
                param($urlQuery)

                $encoded = [System.Net.WebUtility]::HtmlEncode($urlQuery)
                $browser = New-Object psobject
                $browser.Navigate($encoded)
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.InsecureDeserialization" {

    It "Should detect tainted json passed to JsonSerializer.Deserialize" {
        $result = RunRuleForCommand {
            function Test-EngineSchema
            {
                param($pathInput)

                if($null -ne $pathInput)
                {
                    $json = [System.IO.File]::ReadAllText($pathInput)
                    $schema = [System.Text.Json.JsonSerializer]::Deserialize($json)
                    return $schema
                }
            }
        }

        $result.RuleName | Should -Be "PSSec.InsecureDeserialization"
    }

    It "Should detect tainted input in ConvertFrom-Json" {
        $result = RunRuleForCommand {
            function Read-Config
            {
                param($jsonInput)
                $obj = ConvertFrom-Json -InputObject $jsonInput
                return $obj
            }
        }

        $result.RuleName | Should -Be "PSSec.InsecureDeserialization"
    }

    It "Should allow constant json deserialization" {
        $result = RunRuleForCommand {
            function Read-Config
            {
                $json = '{"name":"safe"}'
                $obj = ConvertFrom-Json -InputObject $json
                return $obj
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.OldTlsProtocol" {

    It "Should detect old TLS protocol enum" {
        $result = RunRuleForCommand {
            function Invoke-ClientAuth
            {
                $protocol = [System.Security.Authentication.SslProtocols]::Tls
                return $protocol
            }
        }

        $result.RuleName | Should -Be "PSSec.OldTlsProtocol"
    }

    It "Should allow TLS 1.2" {
        $result = RunRuleForCommand {
            function Invoke-ClientAuth
            {
                $protocol = [System.Security.Authentication.SslProtocols]::Tls12
                return $protocol
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.OutdatedCrypto" {

    It "Should detect SHA1 provider" {
        $result = RunRuleForCommand {
            function Get-HashValue
            {
                param($text)
                $enc = [System.Text.Encoding]::UTF8
                $buffer = $enc.GetBytes($text)
                $sha1 = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
                return $sha1.ComputeHash($buffer)
            }
        }

        $result.RuleName | Should -Be "PSSec.OutdatedCrypto"
    }

    It "Should allow SHA256 provider" {
        $result = RunRuleForCommand {
            function Get-HashValue
            {
                param($text)
                $enc = [System.Text.Encoding]::UTF8
                $buffer = $enc.GetBytes($text)
                $sha256 = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
                return $sha256.ComputeHash($buffer)
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.Xxe" {

    It "Should detect insecure XmlDocument settings with tainted input" {
        $result = RunRuleForCommand {
            function Read-XmlUnsafe
            {
                param($xmlPathInput)

                $doc = New-Object System.Xml.XmlDocument
                $doc.XmlResolver = New-Object System.Xml.XmlUrlResolver
                $doc.Load($xmlPathInput)
                return $doc.InnerText
            }
        }

        $result.RuleName | Should -Be "PSSec.Xxe"
    }

    It "Should detect insecure XmlReaderSettings with DTD parse" {
        $result = RunRuleForCommand {
            function Read-XmlUnsafe
            {
                param($xmlPathInput)

                $settings = New-Object System.Xml.XmlReaderSettings
                $settings.XmlResolver = New-Object System.Xml.XmlUrlResolver
                $settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse
                $reader = [System.Xml.XmlReader]::Create($xmlPathInput, $settings)
                return $reader
            }
        }

        $result.RuleName | Should -Be "PSSec.Xxe"
    }

    It "Should allow secure XmlReaderSettings" {
        $result = RunRuleForCommand {
            function Read-XmlSafe
            {
                param($xmlPathInput)

                $settings = New-Object System.Xml.XmlReaderSettings
                $settings.XmlResolver = $null
                $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
                $reader = [System.Xml.XmlReader]::Create($xmlPathInput, $settings)
                return $reader
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.Xee" {

    It "Should detect XML entity expansion risk with unbounded settings" {
        $result = RunRuleForCommand {
            function Read-XmlUnsafe
            {
                param($xmlInput)

                $settings = New-Object System.Xml.XmlReaderSettings
                $settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse
                $settings.MaxCharactersFromEntities = 0
                $reader = [System.Xml.XmlReader]::Create($xmlInput, $settings)
                return $reader
            }
        }

        $result.RuleName | Should -Be "PSSec.Xee"
    }

    It "Should allow bounded and non-parse settings" {
        $result = RunRuleForCommand {
            function Read-XmlSafe
            {
                param($xmlInput)

                $settings = New-Object System.Xml.XmlReaderSettings
                $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
                $settings.MaxCharactersFromEntities = 1024
                $reader = [System.Xml.XmlReader]::Create($xmlInput, $settings)
                return $reader
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.SessionTimeout" {

    It "Should detect negative timeout assignment" {
        $result = RunRuleForCommand {
            function Set-Session
            {
                $session = [PSCustomObject]@{}
                $session.Timeout = -1
                return $session
            }
        }

        $result.RuleName | Should -Be "PSSec.SessionTimeout"
    }

    It "Should detect excessive timeout assignment" {
        $result = RunRuleForCommand {
            function Set-Session
            {
                $session = [PSCustomObject]@{}
                $session.Timeout = 120
                return $session
            }
        }

        $result.RuleName | Should -Be "PSSec.SessionTimeout"
    }

    It "Should allow normal timeout assignment" {
        $result = RunRuleForCommand {
            function Set-Session
            {
                $session = [PSCustomObject]@{}
                $session.Timeout = 30
                return $session
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.Ssrf" {

    It "Should detect tainted URL in Invoke-WebRequest" {
        $result = RunRuleForCommand {
            function Invoke-Remote
            {
                param($url)
                Invoke-WebRequest -Uri $url
            }
        }

        $result.RuleName | Should -Be "PSSec.Ssrf"
    }

    It "Should detect tainted endpoint in Invoke-RestMethod" {
        $result = RunRuleForCommand {
            function Invoke-Remote
            {
                param($endpoint)
                $target = "http://$endpoint/api"
                Invoke-RestMethod -Uri $target
            }
        }

        $result.RuleName | Should -Be "PSSec.Ssrf"
    }

    It "Should allow constant URL request" {
        $result = RunRuleForCommand {
            function Invoke-Remote
            {
                $uri = "https://api.example.com/status"
                Invoke-RestMethod -Uri $uri
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.LogInjection" {

    It "Should detect tainted message in Write-EventLog" {
        $result = RunRuleForCommand {
            function Write-AppEvent
            {
                param($message)
                Write-EventLog -LogName Application -Source Demo -EntryType Information -EventId 1 -Message $message
            }
        }

        $result.RuleName | Should -Be "PSSec.LogInjection"
    }

    It "Should detect tainted message in Add-Content" {
        $result = RunRuleForCommand {
            function Write-AppLog
            {
                param($userInput)
                $line = "user=$userInput"
                Add-Content -Path .\app.log -Value $line
            }
        }

        $result.RuleName | Should -Be "PSSec.LogInjection"
    }

    It "Should allow escaped message before logging" {
        $result = RunRuleForCommand {
            function Write-AppLog
            {
                param($message)
                $safe = [System.Security.SecurityElement]::Escape($message)
                Add-Content -Path .\app.log -Value $safe
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.LdapInjection" {

    It "Should detect tainted LDAP filter assignment" {
        $result = RunRuleForCommand {
            function Search-LdapUnsafe
            {
                param($userName, $password)
                $filter = "(&(userId=$userName)(UserPassword=$password))"
                $searcher = New-Object psobject
                $searcher.Filter = $filter
            }
        }

        $result.RuleName | Should -Be "PSSec.LdapInjection"
    }

    It "Should detect tainted LDAPFilter command parameter" {
        $result = RunRuleForCommand {
            function Search-LdapUnsafe2
            {
                param($employeeName)
                $ldap = "(&(objectClass=user)(employeename=$employeeName))"
                Get-ADUser -LDAPFilter $ldap
            }
        }

        $result.RuleName | Should -Be "PSSec.LdapInjection"
    }

    It "Should allow escaped LDAP values" {
        $result = RunRuleForCommand {
            function Search-LdapSafe
            {
                param($userName, $password)
                $safeUser = [Microsoft.Security.Application.Encoder]::LdapFilterEncode($userName)
                $safePwd = [Microsoft.Security.Application.Encoder]::LdapFilterEncode($password)
                $filter = "(&(userId=$safeUser)(UserPassword=$safePwd))"
                $searcher = New-Object psobject
                $searcher.Filter = $filter
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.SensitiveErrorExposure" {

    It "Should detect stack trace exposure in catch" {
        $result = RunRuleForCommand {
            function Show-ErrorUnsafe
            {
                param($value)
                try
                {
                    [int]::Parse($value) | Out-Null
                }
                catch
                {
                    Write-Output $_.ScriptStackTrace
                }
            }
        }

        $result.RuleName | Should -Be "PSSec.SensitiveErrorExposure"
    }

    It "Should detect raw exception output in catch" {
        $result = RunRuleForCommand {
            function Show-ErrorUnsafe2
            {
                param($value)
                try
                {
                    [int]::Parse($value) | Out-Null
                }
                catch
                {
                    Write-Host $_
                }
            }
        }

        $result.RuleName | Should -Be "PSSec.SensitiveErrorExposure"
    }

    It "Should allow generic error message" {
        $result = RunRuleForCommand {
            function Show-ErrorSafe
            {
                param($value)
                try
                {
                    [int]::Parse($value) | Out-Null
                }
                catch
                {
                    Write-Output "An error has occurred."
                }
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.XPathInjection" {

    It "Should detect tainted XPath expression in Evaluate" {
        $result = RunRuleForCommand {
            function Query-XmlUnsafe
            {
                param($username, $passwordHash)
                $query = "//users/user[username/text()='$username' and passwordHash/text()='$passwordHash']/data/text()"
                $navigator = New-Object psobject
                $navigator.Evaluate($query)
            }
        }

        $result.RuleName | Should -Be "PSSec.XPathInjection"
    }

    It "Should detect tainted XPath in Select-Xml" {
        $result = RunRuleForCommand {
            function Query-XmlUnsafe2
            {
                param($xpathExpr)
                Select-Xml -Path .\users.xml -XPath $xpathExpr
            }
        }

        $result.RuleName | Should -Be "PSSec.XPathInjection"
    }

    It "Should allow escaped XPath values" {
        $result = RunRuleForCommand {
            function Query-XmlSafe
            {
                param($username, $passwordHash)
                $safeUser = [System.Security.SecurityElement]::Escape($username)
                $safeHash = [System.Security.SecurityElement]::Escape($passwordHash)
                $query = "//users/user[username/text()='$safeUser' and passwordHash/text()='$safeHash']/data/text()"
                $navigator = New-Object psobject
                $navigator.Evaluate($query)
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.OpenRedirect" {

    It "Should detect tainted URL in Redirect" {
        $result = RunRuleForCommand {
            function Redirect-Unsafe
            {
                param($redirectUrl)
                $response = New-Object psobject
                $response.Redirect($redirectUrl)
            }
        }

        $result.RuleName | Should -Be "PSSec.OpenRedirect"
    }

    It "Should detect tainted URL in RedirectPermanent" {
        $result = RunRuleForCommand {
            function Redirect-Unsafe2
            {
                param($next)
                $url = $next
                $response = New-Object psobject
                $response.RedirectPermanent($url)
            }
        }

        $result.RuleName | Should -Be "PSSec.OpenRedirect"
    }

    It "Should allow IsLocalUrl validated redirect" {
        $result = RunRuleForCommand {
            function Redirect-Safe
            {
                param($redirectUrl)
                if([Microsoft.AspNet.Membership.OpenAuth.OpenAuth]::IsLocalUrl($redirectUrl))
                {
                    $response = New-Object psobject
                    $response.Redirect($redirectUrl)
                }
            }
        }

        $result | Should -Be $null
    }
}

Describe "Tests for PSSec.TaintedConfig" {

    It "Should detect tainted config in connection string" {
        $result = RunRuleForCommand {
            function Set-DbConfigUnsafe
            {
                param($catalog)
                $dbConnection = New-Object psobject
                $dbConnection.ConnectionString = "Data Source=.;Initial Catalog=$catalog;User ID=sa;Password=pass;"
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.TaintedConfig"
    }

    It "Should detect tainted config command argument" {
        $result = RunRuleForCommand {
            function Set-AppConfigUnsafe
            {
                param($tenant)
                Set-ItemProperty -Path HKLM:\Software\Demo -Name Config -Configuration $tenant
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.TaintedConfig"
    }

    It "Should allow allowlisted configuration value" {
        $result = RunRuleForCommand {
            function Set-DbConfigSafe
            {
                param($catalog, [System.Collections.Generic.HashSet[string]]$validCatalogNames)
                if(-not $validCatalogNames.Contains($catalog))
                {
                    return
                }

                $dbConnection = New-Object psobject
                $dbConnection.ConnectionString = "Data Source=.;Initial Catalog=$catalog;User ID=sa;Password=pass;"
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.VulnerablePackage" {

    It "Should detect vulnerable Google.Protobuf version" {
        $result = RunRuleForCommand {
            $pkg = "Google.Protobuf 3.11.4"
            Write-Output $pkg
        }

        @($result.RuleName) | Should -Contain "PSSec.VulnerablePackage"
    }

    It "Should detect vulnerable SSH.NET version" {
        $result = RunRuleForCommand {
            $pkg = "SSH.NET 2016.1.0"
            Write-Output $pkg
        }

        @($result.RuleName) | Should -Contain "PSSec.VulnerablePackage"
    }

    It "Should allow non-vulnerable package version" {
        $result = RunRuleForCommand {
            $pkg = "Google.Protobuf 3.21.12"
            Write-Output $pkg
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.ReDoS" {

    It "Should detect unsafe regex with tainted input" {
        $result = RunRuleForCommand {
            function Test-ReDoSUnsafe
            {
                param($name)
                $pattern = '(x+)+y'
                [regex]::IsMatch($name, $pattern)
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.ReDoS"
    }

    It "Should detect inline unsafe regex pattern" {
        $result = RunRuleForCommand {
            function Test-ReDoSUnsafe2
            {
                param($line)
                [regex]::Match($line, '^(-?\d+)*$').Success
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.ReDoS"
    }

    It "Should allow safe regex pattern" {
        $result = RunRuleForCommand {
            function Test-ReDoSSafe
            {
                param($line)
                [regex]::IsMatch($line, '^(\d{2}-\d{2}-\d{4})$')
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.NoSqlInjection" {

    It "Should detect tainted NoSQL filter string" {
        $result = RunRuleForCommand {
            function Find-UserUnsafe
            {
                param($login, $password)
                $filter = '{"$where":"function(){return this.login==''' + $login + ''' && this.password==''' + $password + ''';}"}'
                $collection = New-Object psobject
                $collection.Find($filter)
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.NoSqlInjection"
    }

    It "Should detect tainted NoSQL command filter" {
        $result = RunRuleForCommand {
            function Delete-Unsafe
            {
                param($count)
                $query = '{"$where":"function(){return this.count == ' + $count + ';}"}'
                Remove-MongoItem -Filter $query
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.NoSqlInjection"
    }

    It "Should allow typed NoSQL filter object" {
        $result = RunRuleForCommand {
            function Find-UserSafe
            {
                param($login, $password)
                $filter = @{ login = $login; password = $password }
                $collection = New-Object psobject
                $collection.Find($filter)
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.ZipSlip" {

    It "Should detect tainted entry name used in ExtractToFile path" {
        $result = RunRuleForCommand {
            function Expand-ZipUnsafe
            {
                param($destinationDirectory, $entryName)
                $extractPath = [System.IO.Path]::Combine($destinationDirectory, $entryName)
                $entry = New-Object psobject
                $entry.ExtractToFile($extractPath, $true)
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.ZipSlip"
    }

    It "Should allow validated extraction full path" {
        $result = RunRuleForCommand {
            function Expand-ZipSafe
            {
                param($destinationDirectory, $entryName)
                $extractPath = [System.IO.Path]::Combine($destinationDirectory, $entryName)
                $extractFullPath = [System.IO.Path]::GetFullPath($extractPath)
                $destinationDirectoryFullPath = [System.IO.Path]::GetFullPath($destinationDirectory)
                if(-not $extractFullPath.StartsWith($destinationDirectoryFullPath))
                {
                    throw "Zip Slip"
                }

                $entry = New-Object psobject
                $entry.ExtractToFile($extractFullPath, $true)
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.InvisibleCharacter" {

    It "Should detect zero-width space in code" {
        $result = RunRuleForCommand "`$value = 'ab`u{200B}cd'"

        @($result.RuleName) | Should -Contain "PSSec.InvisibleCharacter"
    }

    It "Should allow normal visible text" {
        $result = RunRuleForCommand {
            $value = 'abcd'
            Write-Output $value
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.CookieInjection" {

    It "Should detect tainted value used for HttpCookie" {
        $result = RunRuleForCommand {
            function Set-CookieUnsafe
            {
                param($userRole)
                $cookie = New-Object System.Web.HttpCookie 'role', $userRole
                return $cookie
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.CookieInjection"
    }

    It "Should allow validated cookie value" {
        $result = RunRuleForCommand {
            function Set-CookieSafe
            {
                param($cultureValue)
                if(-not [regex]::IsMatch($cultureValue, '^[a-zA-Z0-9_-]{2,20}$'))
                {
                    return
                }

                $cookie = New-Object System.Web.HttpCookie 'culture', $cultureValue
                return $cookie
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.ExternallyControlledFormatString" {

    It "Should detect tainted format string in string.Format" {
        $result = RunRuleForCommand {
            function Apply-UnsafeFormat
            {
                param($format, $name)
                [string]::Format($format, $name)
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.ExternallyControlledFormatString"
    }

    It "Should allow constant format string" {
        $result = RunRuleForCommand {
            function Apply-SafeFormat
            {
                param($name)
                $format = "Hello {0}"
                [string]::Format($format, $name)
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.ExcessiveFilePermission" {

    It "Should detect overly broad chmod numeric mode" {
        $result = RunRuleForCommand {
            function Set-UnsafePermission
            {
                param($path)
                chmod 777 $path
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.ExcessiveFilePermission"
    }

    It "Should allow least-privilege chmod mode" {
        $result = RunRuleForCommand {
            function Set-SafePermission
            {
                param($path)
                chmod 640 $path
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.PredictableRandomSeed" {

    It "Should detect constant random seed" {
        $result = RunRuleForCommand {
            function Get-UnsafeNonce
            {
                $rnd = [System.Random]::new(4040)
                return $rnd.Next()
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.PredictableRandomSeed"
    }

    It "Should allow cryptographic RNG usage" {
        $result = RunRuleForCommand {
            function Get-SafeNonce
            {
                $bytes = [byte[]]::new(16)
                [System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
                return [Convert]::ToHexString($bytes)
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.CustomCryptographicAlgorithm" {

    It "Should detect class extending HashAlgorithm" {
        $result = RunRuleForCommand {
            class MyCustomDigest : System.Security.Cryptography.HashAlgorithm
            {
                [void] Initialize() {}
                [byte[]] HashFinal() { return [byte[]]::new(0) }
                [void] HashCore([byte[]]$array, [int]$ibStart, [int]$cbSize) {}
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.CustomCryptographicAlgorithm"
    }

    It "Should allow standard SHA256 usage" {
        $result = RunRuleForCommand {
            function Get-SafeHash
            {
                param([string]$input)
                $sha = [System.Security.Cryptography.SHA256]::Create()
                $bytes = [System.Text.Encoding]::UTF8.GetBytes($input)
                return $sha.ComputeHash($bytes)
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Tests for PSSec.UnrestrictedPosixPermission" {

    It "Should detect chmod granting rights to others" {
        $result = RunRuleForCommand {
            function Set-UnsafePosixMode
            {
                param($path)
                chmod o+rw $path
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.UnrestrictedPosixPermission"
    }

    It "Should allow removing others permissions" {
        $result = RunRuleForCommand {
            function Set-SafePosixMode
            {
                param($path)
                chmod o-rwx,u+rw $path
            }
        }

        @($result).Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.ExcessiveFilePermission" {

    It "Should detect Vulnerable_ExcessiveFilePermission_1.ps1" {
        $result = RunRuleForFile "Vulnerable_ExcessiveFilePermission_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.ExcessiveFilePermission"
    }

    It "Should detect Vulnerable_ExcessiveFilePermission_2.ps1" {
        $result = RunRuleForFile "Vulnerable_ExcessiveFilePermission_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.ExcessiveFilePermission"
    }

    It "Should allow Safe_ExcessiveFilePermission_1.ps1" {
        $result = RunRuleForFile "Safe_ExcessiveFilePermission_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.ExcessiveFilePermission").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.PredictableRandomSeed" {

    It "Should detect Vulnerable_PredictableRandomSeed_1.ps1" {
        $result = RunRuleForFile "Vulnerable_PredictableRandomSeed_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.PredictableRandomSeed"
    }

    It "Should detect Vulnerable_PredictableRandomSeed_2.ps1" {
        $result = RunRuleForFile "Vulnerable_PredictableRandomSeed_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.PredictableRandomSeed"
    }

    It "Should allow Safe_PredictableRandomSeed_1.ps1" {
        $result = RunRuleForFile "Safe_PredictableRandomSeed_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.PredictableRandomSeed").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.CustomCryptographicAlgorithm" {

    It "Should detect Vulnerable_CustomCryptographicAlgorithm_1.ps1" {
        $result = RunRuleForFile "Vulnerable_CustomCryptographicAlgorithm_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.CustomCryptographicAlgorithm"
    }

    It "Should detect Vulnerable_CustomCryptographicAlgorithm_2.ps1" {
        $result = RunRuleForFile "Vulnerable_CustomCryptographicAlgorithm_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.CustomCryptographicAlgorithm"
    }

    It "Should allow Safe_CustomCryptographicAlgorithm_1.ps1" {
        $result = RunRuleForFile "Safe_CustomCryptographicAlgorithm_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.CustomCryptographicAlgorithm").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.UnrestrictedPosixPermission" {

    It "Should detect Vulnerable_UnrestrictedPosixPermission_1.ps1" {
        $result = RunRuleForFile "Vulnerable_UnrestrictedPosixPermission_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.UnrestrictedPosixPermission"
    }

    It "Should detect Vulnerable_UnrestrictedPosixPermission_2.ps1" {
        $result = RunRuleForFile "Vulnerable_UnrestrictedPosixPermission_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.UnrestrictedPosixPermission"
    }

    It "Should allow Safe_UnrestrictedPosixPermission_1.ps1" {
        $result = RunRuleForFile "Safe_UnrestrictedPosixPermission_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.UnrestrictedPosixPermission").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.InsecureCorsWildcardOrigin" {

    It "Should detect wildcard CORS header in hashtable" {
        $result = RunRuleForCommand {
            function Set-CorsUnsafe
            {
                $headers = @{ 'Access-Control-Allow-Origin' = '*' }
                return $headers
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.InsecureCorsWildcardOrigin"
    }

    It "Should allow explicit CORS origin" {
        $result = RunRuleForCommand {
            function Set-CorsSafe
            {
                $headers = @{ 'Access-Control-Allow-Origin' = 'https://allowed.example.com' }
                return $headers
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.InsecureCorsWildcardOrigin").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.EmptyDbPassword" {

    It "Should detect empty password assignment" {
        $result = RunRuleForCommand {
            function Connect-UnsafeDb
            {
                $conn = New-Object psobject
                $conn.Password = ""
                return $conn
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.EmptyDbPassword"
    }

    It "Should allow password from environment" {
        $result = RunRuleForCommand {
            function Connect-SafeDb
            {
                $pwd = $env:DB_PASSWORD
                Invoke-Sqlcmd -ServerInstance 'db01' -Username 'server' -Password $pwd -Query 'SELECT 1'
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.EmptyDbPassword").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.NonAtomicTempFileCreation" {

    It "Should detect delete and recreate temp path flow" {
        $result = RunRuleForCommand {
            function New-TempUnsafe
            {
                $tmp = [System.IO.Path]::GetTempFileName()
                Remove-Item $tmp
                New-Item -ItemType Directory -Path $tmp | Out-Null
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.NonAtomicTempFileCreation"
    }

    It "Should allow direct temp directory creation" {
        $result = RunRuleForCommand {
            function New-TempSafe
            {
                $tmpDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.Guid]::NewGuid().ToString())
                [System.IO.Directory]::CreateDirectory($tmpDir) | Out-Null
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.NonAtomicTempFileCreation").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.HardcodedIpAddress" {

    It "Should detect hardcoded IPv4 literal" {
        $result = RunRuleForCommand {
            function Connect-UnsafeIp
            {
                $ip = '117.107.58.59'
                Test-Connection -ComputerName $ip -Count 1
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.HardcodedIpAddress"
    }

    It "Should allow endpoint from environment variable" {
        $result = RunRuleForCommand {
            function Connect-SafeIp
            {
                $ip = $env:APP_SERVER_IP
                Test-Connection -ComputerName $ip -Count 1
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.HardcodedIpAddress").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.InsecureCorsWildcardOrigin" {

    It "Should detect Vulnerable_InsecureCorsWildcardOrigin_1.ps1" {
        $result = RunRuleForFile "Vulnerable_InsecureCorsWildcardOrigin_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.InsecureCorsWildcardOrigin"
    }

    It "Should detect Vulnerable_InsecureCorsWildcardOrigin_2.ps1" {
        $result = RunRuleForFile "Vulnerable_InsecureCorsWildcardOrigin_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.InsecureCorsWildcardOrigin"
    }

    It "Should allow Safe_InsecureCorsWildcardOrigin_1.ps1" {
        $result = RunRuleForFile "Safe_InsecureCorsWildcardOrigin_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.InsecureCorsWildcardOrigin").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.EmptyDbPassword" {

    It "Should detect Vulnerable_EmptyDbPassword_1.ps1" {
        $result = RunRuleForFile "Vulnerable_EmptyDbPassword_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.EmptyDbPassword"
    }

    It "Should detect Vulnerable_EmptyDbPassword_2.ps1" {
        $result = RunRuleForFile "Vulnerable_EmptyDbPassword_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.EmptyDbPassword"
    }

    It "Should allow Safe_EmptyDbPassword_1.ps1" {
        $result = RunRuleForFile "Safe_EmptyDbPassword_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.EmptyDbPassword").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.NonAtomicTempFileCreation" {

    It "Should detect Vulnerable_NonAtomicTempFileCreation_1.ps1" {
        $result = RunRuleForFile "Vulnerable_NonAtomicTempFileCreation_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.NonAtomicTempFileCreation"
    }

    It "Should detect Vulnerable_NonAtomicTempFileCreation_2.ps1" {
        $result = RunRuleForFile "Vulnerable_NonAtomicTempFileCreation_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.NonAtomicTempFileCreation"
    }

    It "Should allow Safe_NonAtomicTempFileCreation_1.ps1" {
        $result = RunRuleForFile "Safe_NonAtomicTempFileCreation_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.NonAtomicTempFileCreation").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.HardcodedIpAddress" {

    It "Should detect Vulnerable_HardcodedIpAddress_1.ps1" {
        $result = RunRuleForFile "Vulnerable_HardcodedIpAddress_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.HardcodedIpAddress"
    }

    It "Should detect Vulnerable_HardcodedIpAddress_2.ps1" {
        $result = RunRuleForFile "Vulnerable_HardcodedIpAddress_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.HardcodedIpAddress"
    }

    It "Should allow Safe_HardcodedIpAddress_1.ps1" {
        $result = RunRuleForFile "Safe_HardcodedIpAddress_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.HardcodedIpAddress").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.UnencryptedCommunicationChannel" {

    It "Should detect HTTP endpoint in PowerShell web request" {
        $result = RunRuleForCommand {
            function Invoke-UnsafeHttp
            {
                Invoke-WebRequest -Uri 'http://internal.service.local/api'
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.UnencryptedCommunicationChannel"
    }

    It "Should allow HTTPS endpoint" {
        $result = RunRuleForCommand {
            function Invoke-SafeHttps
            {
                Invoke-RestMethod -Uri 'https://internal.service.local/api'
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.UnencryptedCommunicationChannel").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.SensitiveCredentialPattern" {

    It "Should detect GitHub token pattern" {
        $result = RunRuleForCommand {
            function Leak-GitHubToken
            {
                $token = 'ghp_0123456789abcdefghijklmnopqrstuvwxyzAB'
                return $token
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should allow env-based token retrieval" {
        $result = RunRuleForCommand {
            function Use-SafeToken
            {
                $token = $env:API_TOKEN
                return $token
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.SensitiveCredentialPattern").Count | Should -Be 0
    }
}

Describe "Tests for PSSec.AuthenticationBypassSpoofing" {

    It "Should detect REMOTE_ADDR-based trust check" {
        $result = RunRuleForCommand {
            function Test-SpoofableIpAuth
            {
                if($env:REMOTE_ADDR -eq '127.0.0.1')
                {
                    return $true
                }
                return $false
            }
        }

        @($result.RuleName) | Should -Contain "PSSec.AuthenticationBypassSpoofing"
    }

    It "Should allow auth not based on remote host headers" {
        $result = RunRuleForCommand {
            function Test-SafeAuthSignal
            {
                param($userId)
                return ($userId -eq 'admin')
            }
        }

        @($result | Where-Object RuleName -eq "PSSec.AuthenticationBypassSpoofing").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.UnencryptedCommunicationChannel" {

    It "Should detect Vulnerable_UnencryptedCommunicationChannel_1.ps1" {
        $result = RunRuleForFile "Vulnerable_UnencryptedCommunicationChannel_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.UnencryptedCommunicationChannel"
    }

    It "Should detect Vulnerable_UnencryptedCommunicationChannel_2.ps1" {
        $result = RunRuleForFile "Vulnerable_UnencryptedCommunicationChannel_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.UnencryptedCommunicationChannel"
    }

    It "Should allow Safe_UnencryptedCommunicationChannel_1.ps1" {
        $result = RunRuleForFile "Safe_UnencryptedCommunicationChannel_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.UnencryptedCommunicationChannel").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.SensitiveCredentialPattern" {

    It "Should detect Vulnerable_SensitiveCredentialPattern_1.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_2.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_3.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_3.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_4.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_4.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_5.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_5.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_6.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_6.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_7.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_7.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should detect Vulnerable_SensitiveCredentialPattern_8.ps1" {
        $result = RunRuleForFile "Vulnerable_SensitiveCredentialPattern_8.ps1"
        @($result.RuleName) | Should -Contain "PSSec.SensitiveCredentialPattern"
    }

    It "Should allow Safe_SensitiveCredentialPattern_1.ps1" {
        $result = RunRuleForFile "Safe_SensitiveCredentialPattern_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.SensitiveCredentialPattern").Count | Should -Be 0
    }
}

Describe "Example tests for PSSec.AuthenticationBypassSpoofing" {

    It "Should detect Vulnerable_AuthenticationBypassSpoofing_1.ps1" {
        $result = RunRuleForFile "Vulnerable_AuthenticationBypassSpoofing_1.ps1"
        @($result.RuleName) | Should -Contain "PSSec.AuthenticationBypassSpoofing"
    }

    It "Should detect Vulnerable_AuthenticationBypassSpoofing_2.ps1" {
        $result = RunRuleForFile "Vulnerable_AuthenticationBypassSpoofing_2.ps1"
        @($result.RuleName) | Should -Contain "PSSec.AuthenticationBypassSpoofing"
    }

    It "Should allow Safe_AuthenticationBypassSpoofing_1.ps1" {
        $result = RunRuleForFile "Safe_AuthenticationBypassSpoofing_1.ps1"
        @($result | Where-Object RuleName -eq "PSSec.AuthenticationBypassSpoofing").Count | Should -Be 0
    }
}