From ced40b21748295d7cdab0ceeb35348246e23afcc Mon Sep 17 00:00:00 2001 From: NWarila <33955773+NWarila@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:49:48 +0000 Subject: [PATCH] build(analyzers): vendor InjectionHunter for PowerShell injection SAST Vendor Microsoft/Lee Holmes' InjectionHunter ruleset (PowerShell Gallery v1.0.0, frozen since 2017) under analyzers/InjectionHunter/ and wire it into the house PSScriptAnalyzer config via CustomRulePath. It taint-tracks untrusted input into execution contexts (Invoke-Expression, Add-Type, dynamic member / method / property access, cmd/powershell command injection, unsafe escaping) -- a defect class the built-in rules do not catch. The module is pinned, not installed live: InjectionHunter.psd1 and .psm1 are committed byte-identical to the Gallery package (SHA-256 recorded in VENDORED.md), marked -text in .gitattributes so EOL normalization cannot alter the bytes, and allowlisted in the deny-all .gitignore. VENDORED.md records the source, version, GUID, file hashes, the pre-vendoring audit (eight passive AST rule functions; no install logic, network, or obfuscation), and the license status. The CI analyze step excludes the vendored directory from its own lint target set (third-party upstream, not restyled to house rules) while still loading it as a rule provider. tests/InjectionHunter.Tests.ps1 proves the ruleset is loaded through the settings file and fires on Invoke-Expression and Add-Type fixtures while staying clean on safe idiom. Wiring InjectionHunter surfaced three InjectionRisk.UnsafeEscaping false positives in analyzers/HouseRules.psm1, all from benign empty-string -replace operands (stripping scope and namespace prefixes from AST type names). They are resolved by writing the empty replacement as the house-idiomatic [System.String]::Empty -- behavior-identical, no logic change -- which keeps the rule fully active everywhere rather than excluding it. --- .gitattributes | 5 + .github/workflows/ci.yaml | 9 +- .gitignore | 6 + PSScriptAnalyzerSettings.psd1 | 1 + analyzers/HouseRules.psm1 | 10 +- .../InjectionHunter/InjectionHunter.psd1 | Bin 0 -> 28668 bytes .../InjectionHunter/InjectionHunter.psm1 | 569 ++++++++++++++++++ analyzers/InjectionHunter/VENDORED.md | 76 +++ tests/InjectionHunter.Tests.ps1 | 57 ++ 9 files changed, 727 insertions(+), 6 deletions(-) create mode 100644 analyzers/InjectionHunter/InjectionHunter.psd1 create mode 100644 analyzers/InjectionHunter/InjectionHunter.psm1 create mode 100644 analyzers/InjectionHunter/VENDORED.md create mode 100644 tests/InjectionHunter.Tests.ps1 diff --git a/.gitattributes b/.gitattributes index 96a0d0a..75be44b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,11 @@ LICENSE text eol=lf +# Vendored InjectionHunter ruleset: store byte-identical to the PowerShell Gallery +# package (UTF-16 manifest; CRLF + signed module). No EOL normalization. +analyzers/InjectionHunter/InjectionHunter.psd1 -text +analyzers/InjectionHunter/InjectionHunter.psm1 -text + .github/ export-ignore .gitignore export-ignore .gitattributes export-ignore diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd4848b..4479870 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,14 @@ jobs: - name: Run PSScriptAnalyzer shell: pwsh run: | - $results = Invoke-ScriptAnalyzer -Path . -Settings ./PSScriptAnalyzerSettings.psd1 -Recurse + # Lint this repo's own sources. The vendored, pinned InjectionHunter ruleset under + # analyzers/InjectionHunter/ is third-party upstream code loaded via CustomRulePath + # (not ours to restyle to house rules), so it is excluded from the lint target set. + $targets = Get-ChildItem -Path . -Recurse -File -Include '*.ps1', '*.psm1', '*.psd1' | + Where-Object { ($_.FullName -replace '\\', '/') -notlike '*/analyzers/InjectionHunter/*' } + $results = $targets | ForEach-Object { + Invoke-ScriptAnalyzer -Path $_.FullName -Settings ./PSScriptAnalyzerSettings.psd1 + } if ($results) { $results | Format-Table -AutoSize | Out-String | Write-Host $errors = @($results | Where-Object { $_.Severity -eq 'Error' }) diff --git a/.gitignore b/.gitignore index 73c1e95..93ef6ff 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,12 @@ !/analyzers/ !/analyzers/*.psm1 +# Vendored InjectionHunter ruleset (frozen, pinned; see analyzers/InjectionHunter/VENDORED.md) +!/analyzers/InjectionHunter/ +!/analyzers/InjectionHunter/*.psd1 +!/analyzers/InjectionHunter/*.psm1 +!/analyzers/InjectionHunter/*.md + # Module source !/src/ !/src/**/ diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index 541ea8f..7042b69 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -11,6 +11,7 @@ CustomRulePath = @( './analyzers/HouseRules.psm1' + './analyzers/InjectionHunter/InjectionHunter.psd1' ) IncludeDefaultRules = $true diff --git a/analyzers/HouseRules.psm1 b/analyzers/HouseRules.psm1 index 04a63ff..f406e6d 100644 --- a/analyzers/HouseRules.psm1 +++ b/analyzers/HouseRules.psm1 @@ -167,7 +167,7 @@ Function Get-HouseRuleVariableName { $VariableAst.VariablePath.IsScript -eq $False -and $VariableAst.VariablePath.IsGlobal -eq $False ) { - [System.String]($VariableAst.VariablePath.UserPath -replace '(?i)^(private|local):', '') + [System.String]($VariableAst.VariablePath.UserPath -replace '(?i)^(private|local):', [System.String]::Empty) } } @@ -926,9 +926,9 @@ Function Get-HouseRuleFunctionAttribute { ( ([System.String]$PSItem.TypeName.FullName) -replace '^(System\.Management\.Automation\.)?', - '' + [System.String]::Empty ) -replace 'Attribute$', - '' + [System.String]::Empty ) -ieq $AttributeName } } @@ -961,9 +961,9 @@ Function Get-HouseRuleAttributeName { ( ([System.String]$AttributeAst.TypeName.FullName) -replace '^(System\.Management\.Automation\.)?', - '' + [System.String]::Empty ) -replace 'Attribute$', - '' + [System.String]::Empty ) } diff --git a/analyzers/InjectionHunter/InjectionHunter.psd1 b/analyzers/InjectionHunter/InjectionHunter.psd1 new file mode 100644 index 0000000000000000000000000000000000000000..da638d6ac589219249795388b199fca9c6628fed GIT binary patch literal 28668 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 +function Measure-InvokeExpression +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.CommandAst] + if($targetAst) + { + if($targetAst.CommandElements[0].Extent.Text -in ("Invoke-Expression", "iex")) + { + return $true; + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible script injection risk via the Invoke-Expression cmdlet. Untrusted input can cause " + + "arbitrary PowerShell expressions to be run. Variables may be used directly for dynamic parameter arguments, " + + "splatting can be used for dynamic parameter names, and the invocation operator can be used for dynamic " + + "command names. If content escaping is truly needed, PowerShell has several valid quote characters, so " + + "[System.Management.Automation.Language.CodeGeneration]::Escape* should be used." + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.InvokeExpression" + "Severity" = "Warning" } + } +} + + +function Measure-AddType +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.CommandAst] + if($targetAst) + { + if($targetAst.CommandElements[0].Extent.Text -eq "Add-Type") + { + $addTypeParameters = [System.Management.Automation.Language.StaticParameterBinder]::BindCommand($targetAst) + $typeDefinitionParameter = $addTypeParameters.BoundParameters.TypeDefinition + + ## If it's not a constant value, check if it's a variable with a constant value + if(-not $typeDefinitionParameter.ConstantValue) + { + if($addTypeParameters.BoundParameters.TypeDefinition.Value -is [System.Management.Automation.Language.VariableExpressionAst]) + { + $variableName = $addTypeParameters.BoundParameters.TypeDefinition.Value.VariablePath.UserPath + $constantAssignmentForVariable = $ScriptBlockAst.FindAll( { + param( + [System.Management.Automation.Language.Ast] $Ast + ) + + $assignmentAst = $Ast -as [System.Management.Automation.Language.AssignmentStatementAst] + if($assignmentAst -and + ($assignmentAst.Left.VariablePath.UserPath -eq $variableName) -and + ($assignmentAst.Right.Expression -is [System.Management.Automation.Language.ConstantExpressionAst])) + { + return $true + } + }, $true) + + if($constantAssignmentForVariable) + { + return $false + } + else + { + return $true + } + } + + return $true + } + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible code injection risk via the Add-Type cmdlet. Untrusted input can cause " + + "arbitrary Win32 code to be run." + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.AddType" + "Severity" = "Warning" } + } +} + + +<# +.DESCRIPTION + Finds instances of dangerous methods, which can be used to invoke arbitrary + code if supplied with untrusted input. +#> +function Measure-DangerousMethod +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.InvokeMemberExpressionAst] + if($targetAst) + { + if($targetAst.Member.Extent.Text -in ("InvokeScript", "CreateNestedPipeline", "AddScript", "NewScriptBlock", "ExpandString")) + { + return $true + } + + if(($targetAst.Member.Extent.Text -eq "Create") -and + ($targetAst.Expression.Extent.Text -match "ScriptBlock")) + { + return $true + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible script injection risk via the a dangerous method. Untrusted input can cause " + + "arbitrary PowerShell expressions to be run. The PowerShell.AddCommand().AddParameter() APIs " + + "should be used instead." + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.$($foundNode.Member.Extent.Text)" + "Severity" = "Warning" } + } +} + + +<# +.DESCRIPTION + Finds instances of command invocation with user input, which can be abused for + command injection. +#> +function Measure-CommandInjection +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + ## Finds CommandAst nodes that invoke PowerShell or CMD with user input + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.CommandAst] + if($targetAst) + { + if($targetAst.CommandElements[0].Extent.Text -match "cmd|powershell") + { + $commandInvoked = $targetAst.CommandElements[1] + for($parameterPosition = 1; $parameterPosition -lt $targetAst.CommandElements.Count; $parameterPosition++) + { + if($targetAst.CommandElements[$parameterPosition].Extent.Text -match "/c|/k|command") + { + $commandInvoked = $targetAst.CommandElements[$parameterPosition + 1] + break + } + } + + if($commandInvoked -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) + { + return $true + } + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible command injection risk via calling cmd.exe or powershell.exe. Untrusted input can cause " + + "arbitrary commands to be run. Input should be provided as variable input directly (such as " + + "'cmd /c ping `$destination', rather than within an expandable string." + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.CommandInjection" + "Severity" = "Warning" } + } +} + + +<# +.DESCRIPTION + Finds instances of Foreach-Object used with non-constant member names, which can be abused for + arbitrary member access / invocation when supplied with untrusted user input. +#> +function Measure-ForeachObjectInjection +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + ## Finds CommandAst nodes that invoke Foreach-Object with user input + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.CommandAst] + if($targetAst) + { + if($targetAst.CommandElements[0].Extent.Text -match "foreach|%") + { + $memberInvoked = $targetAst.CommandElements[1] + for($parameterPosition = 1; $parameterPosition -lt $targetAst.CommandElements.Count; $parameterPosition++) + { + if($targetAst.CommandElements[$parameterPosition].Extent.Text -match "Process|MemberName") + { + $memberInvoked = $targetAst.CommandElements[$parameterPosition + 1] + break + } + } + + if((-not ($memberInvoked -is [System.Management.Automation.Language.ConstantExpressionAst])) -and + (-not ($memberInvoked -is [System.Management.Automation.Language.ScriptBlockExpressionAst]))) + { + return $true + } + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible property access injection via Foreach-Object. Untrusted input can cause " + + "arbitrary properties /methods to be accessed: " + $foundNode.Extent + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.ForeachObjectInjection" + "Severity" = "Warning" } + } +} + +<# +.DESCRIPTION + Finds instances of dynamic static property access, which can be vulnerable to property injection if + supplied with untrusted user input. +#> +function Measure-PropertyInjection +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + ## Finds MemberExpressionAst that uses a non-constant member + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.MemberExpressionAst] + $methodAst = $Ast -as [System.Management.Automation.Language.InvokeMemberExpressionAst] + if($targetAst -and (-not $methodAst)) + { + if(-not ($targetAst.Member -is [System.Management.Automation.Language.ConstantExpressionAst])) + { + ## This is not constant access, therefore dangerous + return $true + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible property access injection via dynamic member access. Untrusted input can cause " + + "arbitrary static properties to be accessed: " + $foundNode.Extent + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.StaticPropertyInjection" + "Severity" = "Warning" } + } +} + + +<# +.DESCRIPTION + Finds instances of dynamic method invocation, which can be used to invoke arbitrary + methods if supplied with untrusted input. +#> +function Measure-MethodInjection +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + ## Finds MemberExpressionAst nodes that don't invoke a constant expression + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.InvokeMemberExpressionAst] + if($targetAst) + { + if(-not ($targetAst.Member -is [System.Management.Automation.Language.ConstantExpressionAst])) + { + return $true + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible property access injection via dynamic member access. Untrusted input can cause " + + "arbitrary static properties to be accessed: " + $foundNode.Extent + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.MethodInjection" + "Severity" = "Warning" } + } +} + +<# +.DESCRIPTION + Finds instances of unsafe string escaping, which is then likely to be used in a situation (like Invoke-Expression) + where it is unsafe to use. methods if supplied with untrusted input. + [System.Management.Automation.Language.CodeGeneration]::Escape* should be used instead. +#> +function Measure-UnsafeEscaping +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + ## Finds replace operators likely being used to escape strings improperly + [ScriptBlock] $predicate = { + param ([System.Management.Automation.Language.Ast] $Ast) + + $targetAst = $Ast -as [System.Management.Automation.Language.BinaryExpressionAst] + if($targetAst) + { + if(($targetAst.Operator -match "replace") -and + ($targetAst.Right.Extent.Text -match '`"|''''')) + { + return $true + } + } + } + + $foundNode = $ScriptBlockAst.Find($predicate, $false) + if($foundNode) + { + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{ + "Message" = "Possible unsafe use of input escaping. Variables may be used directly for dynamic parameter arguments, " + + "splatting can be used for dynamic parameter names, and the invocation operator can be used for dynamic " + + "command names. If content escaping is truly needed, PowerShell has several valid quote characters, so " + + "[System.Management.Automation.Language.CodeGeneration]::Escape* should be used instead." + "Extent" = $foundNode.Extent + "RuleName" = "InjectionRisk.UnsafeEscaping" + "Severity" = "Warning" } + } +} +# SIG # Begin signature block +# MIIasAYJKoZIhvcNAQcCoIIaoTCCGp0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB +# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR +# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU7Nh7vd96BGRGZVJRJ/A6hoEu +# pAegghWDMIIEwzCCA6ugAwIBAgITMwAAALfuAa/68MeouwAAAAAAtzANBgkqhkiG +# 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G +# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw +# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTYwOTA3MTc1ODQ1 +# WhcNMTgwOTA3MTc1ODQ1WjCBszELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp +# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw +# b3JhdGlvbjENMAsGA1UECxMETU9QUjEnMCUGA1UECxMebkNpcGhlciBEU0UgRVNO +# OkJCRUMtMzBDQS0yREJFMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT +# ZXJ2aWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuCMjQSw3ep1m +# SndFRK0xgVRgm9wSl3i2llRtDdxzAWN9gQtYAE3hJP0/pV/7HHkshYPfMIRf7Pm/ +# dxSsAN+7ATnNUk+wpe46rfe0FDNxoE6CYaiMSNjKcMXH55bGXNnwrrcsMaZrVXzS +# IQcmAhUQw1jdLntbdTyCAwJ2UqF/XmVtWV/U466G8JP8VGLddeaucY0YKhgYwMnt +# Sp9ElCkVDcUP01L9pgn9JmKUfD3yFt2p1iZ9VKCrlla10JQwe7aNW7xjzXxvcvlV +# IXeA4QSabo4dq8HUh7JoYMqh3ufr2yNgTs/rSxG6D5ITcI0PZkH4PYjO2GbGIcOF +# RVOf5RxVrwIDAQABo4IBCTCCAQUwHQYDVR0OBBYEFJZnqouaH5kw+n1zGHTDXjCT +# 5OMAMB8GA1UdIwQYMBaAFCM0+NlSRnAK7UD7dvuzK7DDNbMPMFQGA1UdHwRNMEsw +# SaBHoEWGQ2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz +# L01pY3Jvc29mdFRpbWVTdGFtcFBDQS5jcmwwWAYIKwYBBQUHAQEETDBKMEgGCCsG +# AQUFBzAChjxodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY3Jv +# c29mdFRpbWVTdGFtcFBDQS5jcnQwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZI +# hvcNAQEFBQADggEBAG7J+Fdd7DgxG6awnA8opmQfW5DHnNDC/JPLof1sA8Nqczym +# cnWIHmlWhqA7TUy4q02lKenO+R/vbmHna1BrC/KkczAyhOzkI2WFU3PeYubv8EjK +# fYPmrNvS8fCsHJXj3N6fuFwXkHmCVBjTchK93auG09ckBYx5Mt4zW0TUbbw4/QAZ +# X64rbut6Aw/C1bpxqBb8vvMssBB9Hw2m8ApFTApaEVOE/sKemVlq0VIo0fCXqRST +# Lb6/QOav3S8S+N34RBNx/aKKOFzBDy6Ni45QvtRfBoNX3f4/mm4TFdNs+SeLQA+0 +# oBs7UgdoxGSpX6vsWaH8dtlBw3NZK7SFi9bBMI4wggTtMIID1aADAgECAhMzAAAB +# eXwuV05S4crWAAEAAAF5MA0GCSqGSIb3DQEBBQUAMHkxCzAJBgNVBAYTAlVTMRMw +# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN +# aWNyb3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29mdCBDb2RlIFNp +# Z25pbmcgUENBMB4XDTE3MDgxMTIwMTExNVoXDTE4MDgxMTIwMTExNVowgYMxCzAJ +# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k +# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsTBE1PUFIx +# HjAcBgNVBAMTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjCCASIwDQYJKoZIhvcNAQEB +# BQADggEPADCCAQoCggEBAKgp/tQQyP9VCp6ZAANSj9ywv/mr+FH+XIxUwifOTCuW +# 69uBHMuGK3nKdX64Z4Mmhr3WLxw+x1iqj2+V+1r8p8YbwcPoTBdOIj23W1Zcf9da +# 9S26u6YJvwZ87pj+QPkwuGv+QG90s7jWOEnJ0IcHLzHftrxOo9Cet2J7VnB1T2e/ +# Bcyjrr4AksIbUKFhOxAAAbGG0CnzQPUP2aMPV6tjCajcqWrnR0OnvhXEPSek6FZS +# iM9ZmaEAhDab0DnSKg0v5gTivxOWiIOpUTcYQYni+YWdjmUaPQNkzMXeUHBd8guF +# qY+xReh3/4OdCbty4OZWCJW5K4MSiTH851hyHb35gyMCAwEAAaOCAWEwggFdMBMG +# A1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT45H6NHGN8AKrMcwBK0/JtOKrN +# gTBSBgNVHREESzBJpEcwRTENMAsGA1UECxMETU9QUjE0MDIGA1UEBRMrMjI5ODAz +# KzFhYmY5ZTVmLWNlZDAtNDJlNi1hNjVkLWQ5MzUwOTU5ZmUwZTAfBgNVHSMEGDAW +# gBTLEejK0rQWWAHJNy4zFha5TJoKHzBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8v +# Y3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNDb2RTaWdQQ0Ff +# MDgtMzEtMjAxMC5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRw +# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY0NvZFNpZ1BDQV8wOC0z +# MS0yMDEwLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAb0trfoYN2AsmUGs6iMhaqfay +# 6iqZp+UGNEQB73P7rS/97fjVgGo1HDTHEwy1XmQ8c2uM8m/Tab7OOw+b+QVyPB1G +# 4eicPjaxbzWpplBUf+HUVz07HnpcjwE/dz9ecydX+qcw59Ryr4vfcSL9iuD64C3f +# X/Led2Tf2rAGAAmrRpCj9f6BhiyTK3XGESjX5YriHCerl4yaxOIHGdPyZBexK93z +# CHp4UIUGMhw5UKPNi3DeCNV7b0w/muh1beTLE1ccKVk4X75Fq6aayvkpns04z7nI +# Bbos+8Qlv2gN3w97QhqVx4+9WmuQC1H617fnj7KzMyhzA1x/o0aCnK22Nnd2hzCC +# BbwwggOkoAMCAQICCmEzJhoAAAAAADEwDQYJKoZIhvcNAQEFBQAwXzETMBEGCgmS +# JomT8ixkARkWA2NvbTEZMBcGCgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UE +# AxMkTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTEwMDgz +# MTIyMTkzMloXDTIwMDgzMTIyMjkzMloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgT +# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m +# dCBDb3Jwb3JhdGlvbjEjMCEGA1UEAxMaTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ +# Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCycllcGTBkvx2aYCAg +# Qpl2U2w+G9ZvzMvx6mv+lxYQ4N86dIMaty+gMuz/3sJCTiPVcgDbNVcKicquIEn0 +# 8GisTUuNpb15S3GbRwfa/SXfnXWIz6pzRH/XgdvzvfI2pMlcRdyvrT3gKGiXGqel +# cnNW8ReU5P01lHKg1nZfHndFg4U4FtBzWwW6Z1KNpbJpL9oZC/6SdCnidi9U3RQw +# WfjSjWL9y8lfRjFQuScT5EAwz3IpECgixzdOPaAyPZDNoTgGhVxOVoIoKgUyt0vX +# T2Pn0i1i8UU956wIAPZGoZ7RW4wmU+h6qkryRs83PDietHdcpReejcsRj1Y8wawJ +# XwPTAgMBAAGjggFeMIIBWjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTLEejK +# 0rQWWAHJNy4zFha5TJoKHzALBgNVHQ8EBAMCAYYwEgYJKwYBBAGCNxUBBAUCAwEA +# ATAjBgkrBgEEAYI3FQIEFgQU/dExTtMmipXhmGA7qDFvpjy82C0wGQYJKwYBBAGC +# NxQCBAweCgBTAHUAYgBDAEEwHwYDVR0jBBgwFoAUDqyCYEBWJ5flJRP8KuEKU5VZ +# 5KQwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br +# aS9jcmwvcHJvZHVjdHMvbWljcm9zb2Z0cm9vdGNlcnQuY3JsMFQGCCsGAQUFBwEB +# BEgwRjBEBggrBgEFBQcwAoY4aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j +# ZXJ0cy9NaWNyb3NvZnRSb290Q2VydC5jcnQwDQYJKoZIhvcNAQEFBQADggIBAFk5 +# Pn8mRq/rb0CxMrVq6w4vbqhJ9+tfde1MOy3XQ60L/svpLTGjI8x8UJiAIV2sPS9M +# uqKoVpzjcLu4tPh5tUly9z7qQX/K4QwXaculnCAt+gtQxFbNLeNK0rxw56gNogOl +# VuC4iktX8pVCnPHz7+7jhh80PLhWmvBTI4UqpIIck+KUBx3y4k74jKHK6BOlkU7I +# G9KPcpUqcW2bGvgc8FPWZ8wi/1wdzaKMvSeyeWNWRKJRzfnpo1hW3ZsCRUQvX/Ta +# rtSCMm78pJUT5Otp56miLL7IKxAOZY6Z2/Wi+hImCWU4lPF6H0q70eFW6NB4lhhc +# yTUWX92THUmOLb6tNEQc7hAVGgBd3TVbIc6YxwnuhQ6MT20OE049fClInHLR82zK +# wexwo1eSV32UjaAbSANa98+jZwp0pTbtLS8XyOZyNxL0b7E8Z4L5UrKNMxZlHg6K +# 3RDeZPRvzkbU0xfpecQEtNP7LN8fip6sCvsTJ0Ct5PnhqX9GuwdgR2VgQE6wQuxO +# 7bN2edgKNAltHIAxH+IOVN3lofvlRxCtZJj/UBYufL8FIXrilUEnacOTj5XJjdib +# Ia4NXJzwoq6GaIMMai27dmsAHZat8hZ79haDJLmIz2qoRzEvmtzjcT3XAH5iR9HO +# iMm4GPoOco3Boz2vAkBq/2mbluIQqBC0N1AI1sM9MIIGBzCCA++gAwIBAgIKYRZo +# NAAAAAAAHDANBgkqhkiG9w0BAQUFADBfMRMwEQYKCZImiZPyLGQBGRYDY29tMRkw +# FwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0MS0wKwYDVQQDEyRNaWNyb3NvZnQgUm9v +# dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcwNDAzMTI1MzA5WhcNMjEwNDAz +# MTMwMzA5WjB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G +# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw +# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwggEiMA0GCSqGSIb3DQEB +# AQUAA4IBDwAwggEKAoIBAQCfoWyx39tIkip8ay4Z4b3i48WZUSNQrc7dGE4kD+7R +# p9FMrXQwIBHrB9VUlRVJlBtCkq6YXDAm2gBr6Hu97IkHD/cOBJjwicwfyzMkh53y +# 9GccLPx754gd6udOo6HBI1PKjfpFzwnQXq/QsEIEovmmbJNn1yjcRlOwhtDlKEYu +# J6yGT1VSDOQDLPtqkJAwbofzWTCd+n7Wl7PoIZd++NIT8wi3U21StEWQn0gASkdm +# EScpZqiX5NMGgUqi+YSnEUcUCYKfhO1VeP4Bmh1QCIUAEDBG7bfeI0a7xC1Un68e +# eEExd8yb3zuDk6FhArUdDbH895uyAc4iS1T/+QXDwiALAgMBAAGjggGrMIIBpzAP +# BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQjNPjZUkZwCu1A+3b7syuwwzWzDzAL +# BgNVHQ8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwgZgGA1UdIwSBkDCBjYAUDqyC +# YEBWJ5flJRP8KuEKU5VZ5KShY6RhMF8xEzARBgoJkiaJk/IsZAEZFgNjb20xGTAX +# BgoJkiaJk/IsZAEZFgltaWNyb3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBSb290 +# IENlcnRpZmljYXRlIEF1dGhvcml0eYIQea0WoUqgpa1Mc1j0BxMuZTBQBgNVHR8E +# STBHMEWgQ6BBhj9odHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9k +# dWN0cy9taWNyb3NvZnRyb290Y2VydC5jcmwwVAYIKwYBBQUHAQEESDBGMEQGCCsG +# AQUFBzAChjhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY3Jv +# c29mdFJvb3RDZXJ0LmNydDATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0B +# AQUFAAOCAgEAEJeKw1wDRDbd6bStd9vOeVFNAbEudHFbbQwTq86+e4+4LtQSooxt +# YrhXAstOIBNQmd16QOJXu69YmhzhHQGGrLt48ovQ7DsB7uK+jwoFyI1I4vBTFd1P +# q5Lk541q1YDB5pTyBi+FA+mRKiQicPv2/OR4mS4N9wficLwYTp2OawpylbihOZxn +# LcVRDupiXD8WmIsgP+IHGjL5zDFKdjE9K3ILyOpwPf+FChPfwgphjvDXuBfrTot/ +# xTUrXqO/67x9C0J71FNyIe4wyrt4ZVxbARcKFA7S2hSY9Ty5ZlizLS/n+YWGzFFW +# 6J1wlGysOUzU9nm/qhh6YinvopspNAZ3GmLJPR5tH4LwC8csu89Ds+X57H2146So +# dDW4TsVxIxImdgs8UoxxWkZDFLyzs7BNZ8ifQv+AeSGAnhUwZuhCEl4ayJ4iIdBD +# 6Svpu/RIzCzU2DKATCYqSCRfWupW76bemZ3KOm+9gSd0BhHudiG/m4LBJ1S2sWo9 +# iaF2YbRuoROmv6pH8BJv/YoybLL+31HIjCPJZr2dHYcSZAI9La9Zj7jkIeW1sMpj +# tHhUBdRBLlCslLCleKuzoJZ1GtmShxN1Ii8yqAhuoFuMJb+g74TKIdbrHk/Jmu5J +# 4PcBZW+JC33Iacjmbuqnl84xKf8OxVtc2E0bodj6L54/LlUWa8kTo/0xggSXMIIE +# kwIBATCBkDB5MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G +# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSMw +# IQYDVQQDExpNaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQQITMwAAAXl8LldOUuHK +# 1gABAAABeTAJBgUrDgMCGgUAoIGwMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEE +# MBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBSt +# 1UOFFkdurYGo9Zol6HDkGEsVazBQBgorBgEEAYI3AgEMMUIwQKAWgBQAUABvAHcA +# ZQByAFMAaABlAGwAbKEmgCRodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vUG93ZXJT +# aGVsbCAwDQYJKoZIhvcNAQEBBQAEggEAKEWDUQKFo6UfYj6LnZ7yE0q5Def7euI7 +# tnZo6op624aqDNVIdhT1pIt4k70q9xplpg8QjoOkQWynS4EmPspzxsaMKqkfcEhK +# ugzqSMRsT/fa8Y6ynZvdCHb860rW1aejzKQyrwjr5Q07aYDbxpmTJZQw3pdacETi +# 1ivI4V0W35DaWgblfPh5rfMwv0nVQNnUY7EJJAEWbLmoumWN3s+Nq9Ch6wa5U0i/ +# vU2Qf7f19i1MJFjt8TVoCLv30Of0iZwnribr00/cXm+0aWIyrAwlocQXN3wDbYw0 +# PZmyhfls1C2sQF3M+Akg58uyzSQEQsH12DZkX5hjF3yipNx+0hHA+qGCAigwggIk +# BgkqhkiG9w0BCQYxggIVMIICEQIBATCBjjB3MQswCQYDVQQGEwJVUzETMBEGA1UE +# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z +# b2Z0IENvcnBvcmF0aW9uMSEwHwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQ +# Q0ECEzMAAAC37gGv+vDHqLsAAAAAALcwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJ +# AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE3MDkwMTIwNDk0OFowIwYJ +# KoZIhvcNAQkEMRYEFMuHlpgxsytXnSc/GJTtVTF/5u5qMA0GCSqGSIb3DQEBBQUA +# BIIBAAIaj+TDVSeHgn8EEuKWJ4D6CuG0U9hckj+5czhTiv4cz/7uIJ21ctmfkeUU +# KGJvyFAZcKwJ5j6Qdb0R0IICVkoK9pZI4VI1rj/tzMezn8gPcw/gufZoP8EE2OP1 +# JPZSUx6sOtnRzK0aLa4bgWF0JefyDLBptc6pnKasMR3xjzX7KXJuGuqUuQvWP1qo +# EP297BXq6/3HSgOEk53vYw5zrLMXKHqPmMJNV2d6ZI+3lcbzWOXgmnuj0Jz1HfAM +# Yd9JGCsyjiPEIVSnHcsIVEJtXSP0LYJ4KNGoNRFfzomgpFMVRTy3JtqMu8FqzJzU +# 6RmXAt3w9qh+LNktk2ptOeCOamc= +# SIG # End signature block diff --git a/analyzers/InjectionHunter/VENDORED.md b/analyzers/InjectionHunter/VENDORED.md new file mode 100644 index 0000000..4137fd7 --- /dev/null +++ b/analyzers/InjectionHunter/VENDORED.md @@ -0,0 +1,76 @@ +# Vendored: InjectionHunter + +A PSScriptAnalyzer `CustomRulePath` ruleset that taint-tracks untrusted input +into PowerShell execution contexts (`Invoke-Expression`, `Add-Type`, dynamic +member/method/property access, `cmd`/`powershell` command injection, unsafe +string escaping). It catches a defect class the built-in PSScriptAnalyzer rules +(e.g. `PSAvoidUsingInvokeExpression`) do not. + +## Source and version + +| Field | Value | +| --- | --- | +| Module | `InjectionHunter` | +| Version | `1.0.0` (the only published version) | +| Source | PowerShell Gallery — `https://www.powershellgallery.com/packages/InjectionHunter/1.0.0` | +| Acquired via | `Save-Module -Name InjectionHunter -RequiredVersion 1.0.0` | +| Module GUID | `a06987c9-e591-4dce-a1e9-b488c3cb26b4` | +| Author | Microsoft Corporation (Lee Holmes) | +| Published (Gallery) | 2017-09-02 | +| Vendored on | 2026-06-23 | +| Upstream status | **Frozen** — unmaintained since 2017; `v1.0.0` is the only release. **Update manually.** | + +## Vendored files (SHA-256) + +Both files are **byte-identical** to the PowerShell Gallery `v1.0.0` package +(verified at vendoring time). + +| File | SHA-256 | Bytes | Encoding | +| --- | --- | --- | --- | +| `InjectionHunter.psm1` | `2ce83368e396c080262befca7c974888def98fb357c57e40e78352d668bf158b` | 26466 | ASCII + Authenticode `# SIG #` block | +| `InjectionHunter.psd1` | `e1d0f5ae6d8e46b871a8649098f9353a1bc7da53fb224f745a2c98e74b16f84e` | 28668 | UTF-16 LE (BOM) | + +Only the manifest (`.psd1`) and root module (`.psm1`) are vendored. The Gallery +package's `InjectionHunter.cat` (catalog signature), `Test-InjectionHunter.ps1`, +and `Tests/` (upstream's own Pester suite) are intentionally **not** vendored — +they are not needed to load the ruleset via `CustomRulePath`, and the house +self-test (`tests/InjectionHunter.Tests.ps1`) proves the rules fire here. + +## Audit (2026-06-23) + +`InjectionHunter.psm1` was read in full before vendoring. It contains **only** +eight PSScriptAnalyzer rule functions — `Measure-InvokeExpression`, +`Measure-AddType`, `Measure-DangerousMethod`, `Measure-CommandInjection`, +`Measure-ForeachObjectInjection`, `Measure-PropertyInjection`, +`Measure-MethodInjection`, `Measure-UnsafeEscaping` — each of which inspects the +analyzed script's AST with a `.Find()` predicate and returns `DiagnosticRecord` +objects. Confirmed: **no install logic, no network calls, no filesystem writes, +no `Invoke-Expression`/`Start-Process`/reflection-load of external code, no +obfuscation** — it is a passive AST analyzer. The `.psm1` carries an expired +(2018) Microsoft Authenticode signature, retained as upstream provenance; the +module is loaded as analyzer rules, not run as a signed script, so expiry is +irrelevant. + +## License + +> [!IMPORTANT] +> InjectionHunter does **not** ship an explicit open-source license. The +> PowerShell Gallery package declares no `LicenseUri` and no SPDX license; its +> manifest copyright is **"(c) Microsoft Corporation 2016. All rights +> reserved."** The historical `github.com/PowerShell/InjectionHunter` repository +> (where an MIT license was once expected) is no longer reachable, and the only +> surviving GitHub fork carries no license file either. + +This copy is retained solely to run InjectionHunter as a PSScriptAnalyzer +`CustomRulePath` ruleset — the use Microsoft published it for. Microsoft's +copyright notice is preserved above and in the vendored manifest. **The +redistribution terms are unverified; whether to vendor (rather than restore at +build time) is an owner decision flagged in the IH-1 REPORT.** + +## How it is wired + +`PSScriptAnalyzerSettings.psd1` adds `./analyzers/InjectionHunter/InjectionHunter.psd1` +to `CustomRulePath`. The CI analyze step excludes this vendored directory from +its own lint target set (third-party, pinned, not restyled to house rules) while +still loading it as a rule provider. `tests/InjectionHunter.Tests.ps1` proves the +ruleset is loaded and effective. diff --git a/tests/InjectionHunter.Tests.ps1 b/tests/InjectionHunter.Tests.ps1 new file mode 100644 index 0000000..ff2b9b1 --- /dev/null +++ b/tests/InjectionHunter.Tests.ps1 @@ -0,0 +1,57 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +Describe 'InjectionHunter SAST ruleset' { + BeforeAll { + If (-not (Get-Module -Name PSScriptAnalyzer)) { + Import-Module -Name PSScriptAnalyzer -ErrorAction Stop + } + + $script:RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path + $script:SettingsPath = Join-Path -Path $script:RepoRoot -ChildPath 'PSScriptAnalyzerSettings.psd1' + + # CustomRulePath entries in the settings file are repo-relative, and PSScriptAnalyzer + # resolves them against the current directory (exactly as the CI analyze step does from + # the repo root). Pushing there proves the settings file itself loads the vendored ruleset. + Push-Location -LiteralPath $script:RepoRoot + } + + AfterAll { + Pop-Location + } + + It 'is loaded through the house settings and fires on Invoke-Expression of untrusted input' { + $ScriptDefinition = @' +param ([string] $UserInput) +Invoke-Expression $UserInput +'@ + + $Results = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -Settings $script:SettingsPath + $Injection = @($Results | Where-Object -FilterScript { $PSItem.RuleName -like 'InjectionRisk.*' }) + + $Injection.RuleName | Should -Contain 'InjectionRisk.InvokeExpression' + } + + It 'fires on Add-Type of a non-constant type definition' { + $ScriptDefinition = @' +param ([string] $UserInput) +Add-Type -TypeDefinition $UserInput +'@ + + $Results = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -Settings $script:SettingsPath + $Injection = @($Results | Where-Object -FilterScript { $PSItem.RuleName -like 'InjectionRisk.*' }) + + $Injection.RuleName | Should -Contain 'InjectionRisk.AddType' + } + + It 'stays clean on a safe parameterised idiom' { + $ScriptDefinition = @' +param ([string] $Name) +Write-Output ('Hello {0}' -f $Name) +'@ + + $Results = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -Settings $script:SettingsPath + $Injection = @($Results | Where-Object -FilterScript { $PSItem.RuleName -like 'InjectionRisk.*' }) + + $Injection | Should -HaveCount 0 + } +}