Are You Safe?

Doctor Scripto

Summary: Windows PowerShell MVP, Jeffery Hicks, talks about profiling scripts.

Microsoft Scripting Guy, Ed Wilson, is here. Today we have as our guest blogger, Jeffery Hicks, Windows PowerShell MVP and author. To read more of Jeff’s previous guest posts, see these Hey, Scripting Guy! Blog posts.

Jeffery Hicks is a Microsoft MVP in Windows PowerShell, a Microsoft Certified Trainer, and an IT veteran with over 20 years of experience. He spent much of this time as an IT consultant specializing in Microsoft server technologies with an emphasis on automation and efficiency. He works today as an independent author, trainer, and consultant. Jeff writes  the popular Professor PowerShell column for MCPMag.com, is a regular contributor to the Petri IT Knowledgebase, 4SysOps, and the Altaro Hyper-V blog. He is a frequent speaker at technology conferences and user groups. If he isn’t writing, Jeff is most likely recording training videos for companies like TrainSignal or hanging out in the forums at PowerShell.org.

Jeff’s latest books are:

Jeff’s contact information:

Blog: The Lonely Administrator
Twitter: twitter.com/jeffhicks
Google Plus: Jeffery Hicks 

In Windows PowerShell 2.0 Best Practices by Ed Wilson, I contributed a side bar about how to test if a script was safe to run. You could blindly run it in a test environment, such as in a virtual machine. But you really should look at it first. However, I realize you may still be learning Windows PowerShell, and perhaps you don’t fully understand everything you might see. So I have something to help.

In Ed’s book, I had a Windows PowerShell script that would “profile” a script. That is, give you an idea of what commands it would run and identify any potentially dangerous commands. Frankly, the previous version was not very well written and never worked the way I imagined it. Fortunately, Windows PowerShell 3.0 introduced a new way of parsing script files by using an Abstract Syntax Tree (or AST).

Don’t worry too much about what that means except that it is now much easier to analyze or parse a Windows PowerShell script. By using AST, I wrote a new Windows PowerShell 3.0 script called Get-ASTScriptProfile.ps1 that finally meets my original goal. 

The script takes the name of a script to “profile.” You can specify a .ps1 or .psm1 file name. By using AST, the script prepares a text report showing you any script requirements, script parameters, commands, and type names. You will see all commands that are used, including those that can’t be resolved and those that I thought might be considered potentially dangerous, such as cmdlets that use the verbs Remove or Stop.

Because some people might invoke methods from .NET classes directly, I’ve also captured all type names such as System.IO.FileInfo. Most type names will probably be related to parameters, but at least you’ll know what to look for. The entire report is turned into a Help topic that is stored in your Documents folder and can be displayed with Get-Help and the awesome –ShowWindow parameter.

For example, I might run a command like this:

PS C:\> C:\scripts\Get-ASTScriptProfile.ps1 C:\scripts\Remove-OldFiles.ps1

In my Documents folder, I’ll get a file called Remove-OldFiles.help.txt, but I’ll also see the report with the Help viewer:

Image of report

The report won’t detail parameters from nested functions, but you’ll still see what commands they will use. The script uses Get-Command to identify commands that might entail loading a module. Most of the time this shouldn’t be an issue, but you still might want to profile the script in a virtualized or test environment.

Of particular interest should be the Warning section, which shows commands that might affect your system. Any unresolved command that you see is either from a module that couldn’t be loaded, or it might be an internally defined command. When you know what to look for, you can open the script in your favorite editor and search for the mystery commands or anything that you don’t understand.

And here is ASTScriptProfile.ps1:

#requires -version 3.0

 

#Get-ASTScriptProfile.ps1

 

<#

.Synopsis

Profile a PowerShell Script

.Description

This script will parse a PowerShell script using the AST to identify elements

and any items that might be dangerous. The output is a text report which by

default is turned into a help topic stored in your Windows PowerShell folder

under Documents.

 

DETAILS

The script takes the name of a script to profile. You can specify a ps1 or

psm1 filename. Using the AST the script will prepare a text report showing

you any script requirements, script parameters, commands and type names. You

will see all commands used including those that can’t be resolved as well as

those that I thought might be considered potentially dangerous such as cmdlets

that use the verbs Remove or Stop. Because some people might invoke methods

from .NET classes directly I’ve also captured all typenames. Most of them will

probably be related to parameters but as least you’ll know what to look for.

 

The report won’t detail parameters from nested functions but you’ll still see

what commands they will use. The script uses Get-Command to identify commands

which might entail loading a module. Most of the time this shouldn’t be an

issue but you still might want to profile the script in virtualized or test

environment.

 

Any unresolved command you see is either from a module that couldn’t be loaded

or it might be an internally defined command. Once you know what to look for

you can open the script in your favorite editor and search for the mystery

commands.

 

.Example

PS C:\> c:\scripts\Get-ASTScriptProfile c:\download\UnknownScript.ps1

 

This will analyze the script UnknownScript.ps1 and show the results in a

help window. It will also create a text file in your documents folder called

UnknownScript.help.txt.

.Link

Get-Command

Get-Alias

#>

 

[cmdletbinding()]

Param(

[Parameter(Position=0,Mandatory,HelpMessage=”Enter the path of a PowerShell script”)]

[ValidateScript({Test-Path $_})]

[string]$Path

)

 

Write-Verbose “Starting $($myinvocation.MyCommand)”

#need to resolve full path

$Path = (Resolve-Path -Path $Path).Path

Write-Verbose “Analyzing $Path”

 

Write-Verbose “Parsing File for AST”

New-Variable astTokens -force

New-Variable astErr -force

 

$AST = [System.Management.Automation.Language.Parser]::ParseFile($Path,[ref]$astTokens,[ref]$astErr)

 

$report=@”

Script Profile report for: $Path

 

“@

 

Write-Verbose “Getting requirements and parameters”

$report+=@”

 

******************

*  Requirements  *

******************

$(($ast.ScriptRequirements | out-string).Trim())

 

******************

*  Parameters    *

******************

$(($ast.ParamBlock.Parameters |

 Select Name,DefaultValue,StaticType,Attributes |

 Format-Table -autosize | Out-String).Trim())

 

“@

 

Write-Verbose “Getting all command elements”

 

$commands = @()

$unresolved = @()

 

$genericCommands = $astTokens |

where {$_.tokenflags -eq ‘commandname’ -AND $_.kind -eq ‘generic’}

 

$aliases = $astTokens |

where {$_.tokenflags -eq ‘commandname’ -AND $_.kind -eq ‘identifier’}

 

Write-Verbose “Parsing commands”

foreach ($command in $genericCommands) {

    Try {

       $commands+= Get-Command -Name $command.text -ErrorAction Stop

    }

    Catch {

      $unresolved+= $command.Text

    }

}

 

foreach ($command in $aliases) {

Try {

       $commands+= Get-Command -Name $command.text -erroraction Stop |

       foreach {

         #get the resolved command

         Get-Command -Name $_.Definition 

       }

    }

    Catch {

        $unresolved+= $command.Text

    }

}

 

 

Write-Verbose “All commands”

$report+=@”

 

******************

*  All Commands  *

******************

$(($Commands | Sort -Unique | Format-Table -autosize | Out-String).Trim())

 

“@

 

Write-Verbose “Unresolved commands”

$report+=@”

 

******************

*  Unresolved    *

******************

$($Unresolved | Sort -Unique | Format-Table -autosize | Out-String)

“@

 

Write-Verbose “Potentially dangerous commands”

#identify dangerous commands

$danger=”Remove”,”Stop”,”Disconnect”,”Suspend”,”Block”,

“Disable”,”Deny”,”Unpublish”,”Dismount”,”Reset”,”Resize”,

“Rename”,”Redo”,”Lock”,”Hide”,”Clear”

 

$danger = $commands | where {$danger -contains $_.verb}

 

#get type names, some of which may come from parameters

Write-Verbose “Typenames”

$report+=@”

 

******************

*  TypeNames     *

******************

$($asttokens | where {$_.tokenflags -eq ‘TypeName’} |

Sort @{expression={$_.text.toupper()}} -unique |

Select -ExpandProperty Text | Out-String)

“@

 

$report+=@”

 

******************

*  Warning       *

******************

$($danger | Format-Table -AutoSize | Out-String)

“@

 

Write-Verbose “Display results”

 

#create a help topic file using the script basename

$basename = (Get-Item $Path).basename    

#stored in the Documents folder

$reportFile = Join-Path -Path “$env:userprofile\Documents” -ChildPath “$basename.help.txt”

 

#insert the Topic line so help recognizes it

“TOPIC” | Out-File -FilePath $reportFile -Encoding ascii

#create the report

$report | Out-File -FilePath $reportFile -Encoding ascii -Append

 

#view the report with Get-Help and -ShowWindow

Get-Help (Join-Path -Path “$env:userprofile\Documents” -ChildPath $basename) -ShowWindow

 

Write-Verbose “Profiling complete”

Practicing safe Windows PowerShell is a habit you have to cultivate, not only in the scripts and tools that you write, but also when using code that is written by someone else. A tool like Get-ASTScriptProfile.ps1 might help. I hope you’ll try it out and let me know what you think.

~Jeff

Thank you Jeff, for sharing your knowledge and time.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

0 comments

Discussion is closed.

Feedback usabilla icon