SearchScript.ps1
|
<# .SYNOPSIS Searches Scripts .DESCRIPTION Searches PowerShell scripts, using the Abstract Syntax Tree. This can quickly and easily find any part of any script in any file. .EXAMPLE # Get every part of every script in this module. Get-Command -Module SearchScript | SearchScript .EXAMPLE # Get every part of every script in the current directory dir *.ps1 | Get-Command { $_ } | SearchScript .EXAMPLE # Get every type reference in the current directory dir *.ps1 | Get-Command { $_ } | SearchScript -For { param($ast) $ast.TypeName } .EXAMPLE # Get every type reference in the current directory # then get their reflected type dir *.ps1 | Get-Command { $_ } | SearchScript -For { param($ast) $ast.TypeName.GetReflectionType } | Foreach-Object { $_.TypeName.GetReflectionType() } .EXAMPLE # Get every command reference in the module search script Get-Command Search-Script | Search-Script -For { param($ast) $ast -is [Management.Automation.Language.CommandAst] } .EXAMPLE # Get every `[Management.Automation.Language.VariableAst]` in Search-Script Get-Command Search-Script | Search-Script -For ([Management.Automation.Language.VariableAst]) .EXAMPLE # Search Script for every `[type]` Get-Command Search-Script | Search-Script -For ([type]) .EXAMPLE # Search-Script for every `[ScriptBlock]` Get-Command Search-Script | Search-Script -For ([ScriptBlock]) .EXAMPLE # Get every `[ScriptBlock]` and `[string]` reference in Search-Script Get-Command Search-Script | Search-Script -For ([ScriptBlock], [string]) .EXAMPLE # Get every `[IComparable]` reference in Search-Script Get-Command Search-Script | Search-Script -For ([IComparable]) .EXAMPLE # Search for scripts that may be impacted by # [CVE-2025-54100](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-54100) Search-Script { Invoke-WebRequest } { param($ast) if (-not $ast.CommandElements -or ( $ast.CommandElements[0] -notmatch 'Invoke-WebRequest|curl|iwr' )) { return $false } if (-not ($ast.CommandElements -match '-UseBasicParsing')) { return $true } return $false } #> [Alias('srsb', 'srScript','SearchScript')] [ArgumentCompleter({ <# .SYNOPSIS History Completer .DESCRIPTION History Argument Completer for a function. This looks thru the current session history for uses of this command. #> param($wordToComplete, $commandAst, $cursorPosition) # Whenever we find a match, we need all elements except the one being completed $upTilNow = foreach ($element in $commandAst.CommandElements) { if ($element.Extent.EndOffset -ge $cursorPosition) { break } $element } # Now, to find any matching history entries, we Get-History @( foreach ($historyItem in Get-History) { # looking for things that are _like_ the entire ast if ($historyItem.CommandLine -like "$upTilNow*") { # and returning the current word(s) to complete # replacing any leading whitespaces, so we don't tab too much. $historyItem.CommandLine.Substring("$upTilNow".Length) -replace '^\s+' } } ) })] param( # The script to search. [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('ScriptBlock','Definition','Haystack')] [ScriptBlock] $Script, # What we're searching for. # # Can be a ScriptBlock, string, regex, type, or ast function. # # If a string is provided, it will be treated as a literal, # unless it starts and ends with `/`. [ValidateScript({ $validTypes = [ScriptBlock], [string], [Regex], [type], [Func[Management.Automation.Language.Ast,bool]] foreach ($validType in $validTypes) { if ($_ -is $validType) { return $true} } throw "must be [$($validTypes -join '] or [')]" })] [Alias('Pattern','Needle','Predicate','SearchScript')] [PSObject] $For = [string]"/./", # If set, will perform a shallow search. # By default will search a scriptblock and nested blocks. [switch] $Shallow ) process { # If the script has no Ast, return if (-not $script.Ast) { return } # If `-For` is a `[String]` or `[Regex]`, # make sure we sanitize our input. #  # If `-For` is a `[string]` if ($for -is [string]) { # the operator is -eq by default. $operator = '-eq' # If it takes the form of a regex literal if ($for -match '^/.+/$') { # strip the slashes $for = $for -replace '^/' -replace '/$' # and match instead. $operator = '-match' } # Always double single quotes to avoid code injection. $For = $for -replace "'","''" # Create a `[Scriptblock]` that finds exactly that string. $for = [ScriptBlock]::Create("param(`$ast) (`$ast.Extent.ToString() $operator '$( $For )') -or (`$ast.Value $operator '$For')") } # If `-For` is a `[Regex]` if ($for -is [Regex]) { $for = # Create a `[ScriptBlock]` that matches that pattern. [ScriptBlock]::Create("param(`$ast) `$pattern = [Regex]::new('$( # Always double single quotes to avoid code injection. $for -replace "'","''" )','$($for.Options)'); `$ast -match `$pattern") } if ($For -as [type[]]) { $for = # Create a `[ScriptBlock]` that looks for that type. # This one is more complicated, so we will create it in two parts [ScriptBlock]::Create(( (@( # dynamically create the list of types 'param($ast)' "`$types = @(" foreach ($forType in $for) { $forType = $forType -as [type] if (-not $forType) { continue } "[$($forType.FullName)]" } ")" ) -join [Environment]::NewLine) + { # Find a reflected type, if there is one. $reflectedType = if ($ast.TypeName.GetReflectionType) { $ast.TypeName.GetReflectionType() } elseif ($ast.StaticType) { $ast.StaticType } else { $null } # Go over each of our potential types # Several conditions would be a use of our type foreach ($type in $types) { # * If the ast is that type, return true if ($ast -is $type) { return $true } if (-not $reflectedType) { continue } # * If the reflected type is exactly that type, return true if ($reflectedType -eq $type) { return $true } # * If the reflected type is a subclass of that type, return true if ($reflectedType.IsSubClassOf($type)) { return $true } # * If the type is an interface, # return true if the reflected type implements it if ($type.IsInterface -and $reflectedType.GetInterface($type)) { return $true } } # Returning nothing will be falsy, and will not return the element. } )) } if ($VerbosePreference -notin 'silentlyContinue', 'ignore') { Write-Verbose "Searching Script For {$for}" } # Call `.FindAll` and let our results flow $Script.Ast.FindAll($for, -not $Shallow) } |