Chapter 10. Scripts

by Mar 18, 2012

PowerShell can be used interactively and in batch mode. All the code that you entered and tested interactively can also be stored in a script file. When you run the script file, the code inside is executed from top to bottom, pretty much like if you had entered the code manually into PowerShell.

So script files are a great way of automating complex tasks that consist of more than just one line of code. Scripts can also serve as a repository for functions you create, so whenever you run a script, it defines all the functions you may need for your daily work.

You can even set up a so called "profile" script which runs automatically each time you launch PowerShell. A profile script is used to set up your personal PowerShell environment. It can set colors, define the prompt, and load additional PowerShell modules and snapins.

Topics Covered:

  • Creating a Script
  • Parameters: Passing Arguments to Scripts

    Creating a Script

    A PowerShell script is a plain text file with the extension ".ps1". You can create it with any text editor or use specialized PowerShell editors like the built-in "Integrated Script Environment" called "ise", or commercial products like "PowerShell Plus".

    You can place any PowerShell code inside your script. When you save the script with a generic text editor, make sure you add the file extension ".ps1".

    If your script is rather short, you could even create it directly from within the console by redirecting the script code to a file:

    ' "Hello world" ' > $env:tempmyscript.ps1
    

    To save multiple lines to a script file using redirection, use "here-strings":

    @'
    $cutoff = (Get-Date) - (New-Timespan -hours 24)
    $filename = "$env:tempreport.txt"
    Get-EventLog -LogName System -EntryType Error,Warning -After $cutoff |
    Format-Table -AutoSize |
    Out-File $filename -width 10000
    
    Invoke-Item $filename
    '@ > $env:tempmyscript.ps1
    
    

    Launching a Script

    To actually run your script, you need to either call the script from within an existing PowerShell window, or prepend the path with "powershell.exe". So, to run the script from within PowerShell, use this:

    & "$env:tempmyscript.ps1"
    

    By prepending the call with "&", you tell PowerShell to run the script in isolation mode. The script runs in its own scope, and all variables and functions defined by the script will be automatically discarded again once the script is done. So this is the perfect way to launch a "job" script that is supposed to just "do something" without polluting your PowerShell environment with left-overs.

    By prepending the call with ".", which is called "dot-sourcing", you tell PowerShell to run the script in global mode. The script now shares the scope with the callers' scope, and functions and variables defined by the script will still be available once the script is done. Use dot-sourcing if you want to debug a script (and for example examine variables), or if the script is a function library and you want to use the functions defined by the script later.

    To run a PowerShell script from outside PowerShell, for example from a batch file, use this line:

    Powershell.exe -noprofile -executionpolicy Bypass -file %TEMP%myscript.ps1
    

    You can use this line within PowerShell as well. Since it always starts a fresh new PowerShell environment, it is a safe way of running a script in a default environment, eliminating interferences with settings and predefined or changed variables and functions.

    Execution Policy – Allowing Scripts to Run

    If you launched your script from outside PowerShell, using an explicit call to powershell.exe, your scripts always ran (unless you mistyped something). That's because here, you submitted the parameter -executionpolicy and turned the restriction off for the particular call.

    To enable PowerShell scripts, you need to change the ExecutionPolicy. There are actually five different execution policies which you can list with this command:

    PS > Get-ExecutionPolicy -List
    
                                      Scope                         ExecutionPolicy
                                      -----                         ---------------
                              MachinePolicy                               Undefined
                                 UserPolicy                               Undefined
                                    process                               Undefined
                                CurrentUser                                  Bypass
                               LocalMachine                            Unrestricted
    
    

    The first two represent group policy settings. They are set to "Undefined" unless you defined ExecutionPolicy with centrally managed group policies in which case they cannot be changed manually.

    Scope "Process" refers to the current PowerShell session only, so once you close PowerShell, this setting gets lost. CurrentUser represents your own user account and applies only to you. LocalMachine applies to all users on your machine, so to change this setting you need local administrator privileges.

    The effective execution policy is the first one from top to bottom that is not set to "Undefined". You can view the effective execution policy like this:

    PS > Get-ExecutionPolicy
    Bypass
    
    

    If all execution policies are "Undefined", the effective execution policy is set to "Restricted".

    Setting Description
    Restricted Script execution is absolutely prohibited.
    Default Standard system setting normally corresponding to "Restricted".
    AllSigned Only scripts having valid digital signatures may be executed. Signatures ensure that the script comes from a trusted source and has not been altered. You'll read more about signatures later on.
    RemoteSigned Scripts downloaded from the Internet or from some other "public" location must be signed. Locally stored scripts may be executed even if they aren't signed. Whether a script is "remote" or "local" is determined by a feature called Zone Identifier, depending on whether your mail client or Internet browser correctly marks the zone. Moreover, it will work only if downloaded scripts are stored on drives formatted with the NTFS file system.
    Unrestricted PowerShell will execute any script.

    Table 10.1: Execution policy setting options

    Many sources recommend changing the execution policy to "RemoteSigned" to allow scripts. This setting will protect you from potentially harmful scripts downloaded from the internet while at the same time, local scripts run fine.

    The mechanism behind the execution policy is just an additional safety net for you. If you feel confident that you won't launch malicious PowerShell code because you carefully check script content before you run scripts, then it is ok to turn off this safety net altogether by setting the execution policy to "Bypass". This setting may be required in some corporate scenarios where scripts are run off file servers that may not be part of your own domain.

    If you must ensure maximum security, you can also set execution policy to "AllSigned". Now, every single script needs to carry a valid digital signature, and if a script was manipulated, PowerShell immediately refuses to run it. Be aware that this setting does require you to be familiar with digital signatures and imposes considerable overhead because it requires you to re-sign any script once you made changes.

    Invoking Scripts like Commands

    To actually invoke scripts just as easily as normal commands—without having to specify relative or absolute paths and the ".ps1" file extension—pick or create a folder to store your scripts in. Next, add this folder to your "Path" environment variable. Done.

    md $env:appdataPSScripts
    copy-item $env:tempmyscript.ps1 $env:appdataPSScriptsmyscript.ps1
    $env:path += ";$env:appdataPSScripts "
    myscript
    
    

    The changes you made to the "Path" environment variable are temporary and only valid in your current PowerShell session. To permanently add a folder to that variable, make sure you append the "Path" environment variable within your special profile script. Since this script runs automatically each time PowerShell starts, each PowerShell session automatically adds your folder to the search path. You learn more about profile scripts in a moment.

    Parameters: Passing Arguments to Scripts

    Scripts can receive additional arguments from the caller in much the same way as functions do (see Chapter 9). Simply add the param() block defining the parameters to the top of your script. You learned about param() blocks in the previous chapter.

    For example, to add parameters to your event log monitoring script, try this:

    @'
    Param(
      $hours = 24,
      [Switch]
      $show
    )
    
    $cutoff = (Get-Date) - (New-Timespan -hours $hours)
    $filename = "$env:tempreport.txt"
    Get-EventLog -LogName System -EntryType Error,Warning -After $cutoff |
    Format-Table -AutoSize |
    Out-File $filename -width 10000
    
    If ($Show) {
      Invoke-Item $filename
    } else {
      Write-Warning "The report has been generated here: $filename"
    }
    '@ > $env:tempmyscript.ps1
    
    

    Now you can run your script and control its behavior by using its parameters. If you copied the script to the folder that you added to your "Path" environment variable, you can even call your script without a path name, almost like a new command:

    PS > copy-item $env:tempmyscript.ps1 $env:appdataPSScriptsmyscript.ps1
    PS > myscript -hours 300
    WARNING: The report has been generated here:
    C:Usersw7-pc9AppDataLocalTempreport.txt
    PS > myscript -hours 300 -show
    
    

    To learn more about parameters, how to make them mandatory or how to add help to your script, refer to the previous chapter. Functions and scripts share the same mechanism.

    Scopes: Variable Visibility

    Any variable or function you define in a script by default is scoped "local". The variable or function is visible from subscopes (like functions or nested functions or scripts called from your script). It is not visible from superscopes (like the one calling the script) unless the script was called dot-sourced.

    So by default, any function or variable you define can be accessed from any other function defined at the same scope or in a subscope:

    function A { "Here is A" }
    function B { "Here is B" }
    
    function C {  B }
    
    C
    
    

    The caller of this script cannot access any function or variable, so the script will not pollute the callers context with left-over functions or variables – unless you call the script dot-sourced like described earlier in this chapter.

    By prefixing variables or function names with one of the following prefixes, you can change the default behavior.

    Script: use this for "shared" variables.
    Global: use this to define variables or functions in the callers' context so they stay visible even after the script finished
    Private: use this to define variables or functions that only exist in the current scope and are invisible to both super- and subscopes.

    Profile Scripts: Automatic Scripts

    Most changes and adjustments you make to PowerShell are only temporary, and once you close and re-open PowerShell, they are lost. To make changes and adjustments persistent, use profile scripts. These scripts get executed automatically whenever PowerShell starts (unless you specify the -noprofile paramater).

    The most widely used profile script is your personal profile script for the current PowerShell host. You find its path in $profile:

    PS > $profile
    C:Usersw7-pc9DocumentsWindowsPowerShellMicrosoft.PowerShell_profile.ps1
    
    

    Since this profile script is specific to your current PowerShell host, the path may look different depending on your host. When you run this command from inside the ISE editor, it looks like this:

    PS > $profile
    C:Usersw7-pc9DocumentsWindowsPowerShellMicrosoft.PowerShellISE_profile.ps1
    
    

    If this file exists, PowerShell runs it automatically. To test whether the script exists, use Test-Path. Here is a little piece of code that creates the profile file if it not yet exists and opens it in notepad so you can add code to it:

    PS > if (!(Test-Path $profile)) { New-Item $profile -Type File -Force | Out-Null
     } notepad $profile
    
    

    There are more profile scripts. $profile.CurrentUserAllHosts returns the path to the script file that automatically runs with all PowerShell hosts, so this is the file to place code in that should execute regardless of the host you use. It executes for both the PowerShell console and the ISE editor.

    $profile.AllUsersCurrentHost is specific to your current host but runs for all users. To create or change this file, you need local administrator privileges. $profile.AllUsersAllHosts runs for all users on all PowerShell hosts. Again, you need local administrator privileges to create or change this file.

    If you use more than one profile script, their execution order is from "general to specific", so the profile script defined in $profile executes last (and if there are conflicting settings, overrides all others).

    Signing Scripts with Digital Signatures

    To guarantee that a script comes from a safe source and wasn't manipulated, scripts can be signed with a digital signature. This signature requires a so called "Codesigning Certificate" which is a digital certificate with a private key and the explicit purpose of validating code. You can get such a certificate from your corporate IT (if they run a PKI infrastructure), or you can buy it from certificate authorities like Verisign or Thawte. You can even create your own "self-signed" certificates which are the least trustworthy alternative.

    Finding Certificates

    To find all codesigning certificates installed in your personal certificate store, use the virtual cert: drive:

    Dir cert:CurrentuserMy -codeSigningCert
        directory: Microsoft.PowerShell.SecurityCertificate::CurrentUserMy
    
    Thumbprint                                Subject
    ----------                                -------
    E24D967BE9519595D7D1AC527B6449455F949C77  CN=PowerShellTestCert
    
    

    The -codeSigningCert parameter ensures that only those certificates are located that are approved for the intended "code signing" purpose and for which you have a private and secret key.

    If you have a choice of several certificates, pick the certificate you want to use for signing by using Where-Object:

    $certificate = Dir cert:CurrentUserMy | 
    Where-Object { $_.Subject -eq "CN=PowerShellTestCert" }

    You can also use low-level -NET methods to open a full-featured selection dialog to pick a certificate:

    
    $Store = New-Object system.security.cryptography.X509Certificates.x509Store("My", "CurrentUser")
    $store.Open("ReadOnly")
    [System.Reflection.Assembly]::LoadWithPartialName("System.Security")
    $certificate = [System.Security.Cryptography.x509Certificates.X509Certificate2UI]::SelectFromCollection($store.certificates,  "Your certificates", "Please select", 0)
    $store.Close()
    $certificate
    Thumbprint                                Subject
    ----------                                -------
    372883FA3B386F72BCE5F475180CE938CE1B8674  CN=MyCertificate
    
    

    Creating/Loading a New Certificate

    If there is no certificate in your certificate store, you cannot sign scripts. You can then either request/purchase a codesigning certificate and install it into your personal certificate store by double-clicking it, or you can temporarily load a certificate file into memory using Get-PfxCertificate.

    Creating Self-Signed Certificates

    The key to making self-signed certificates is the Microsoft tool makecert.exe. Unfortunately, this tool can't be downloaded separately and it may not be spread widely. You have to download it as part of a free "Software Development Kit" (SDK). Makecert.exe is in the .NET framework SDK which you can find at http://msdn2.microsoft.com/en-us/netframework/aa731542.aspx.

    After the SDK is installed, you'll find makecert.exe on your computer and be able to issue a new code-signing certificate with a name you specify by typing the following lines:

    $name = "PowerShellTestCert"
    pushd
    Cd "$env:programfilesMicrosoft Visual Studio 8SDKv2.0Bin"
    .makecert.exe -pe -r -n "CN=$name" -eku 1.3.6.1.5.5.7.3.3 -ss "my"
    popd
    
    

    It will be automatically saved to the CurrentUserMy certificate store. From this location, you can now call and use any other certificate:

    $name = "PowerShellTestCert"
    $certificate = Dir cert:CurrentUserMy | Where-Object { $_.Subject -eq "CN=$name"}
    
    

    Making a Certificate "Trustworthy"

    Certificates you purchased from trusted certificate authorities or your own enterprise IT are considered trustworthy by default. That's because their root is listed in the "trusted root certification authorities container. You can examine these settings like this:

    Certmgr.msc
    

    Self-signed certificates are not trustworthy by default because anyone can create them. To make them trustworthy, you need to copy them into the list of trusted root certification authorities and Trusted Publishers.

    Signing PowerShell Scripts

    PowerShell script signatures require only two things: a valid code-signing certificate and the script that you want to sign. The cmdlet Set-AuthenticodeSignature takes care of the rest.

    The following code grabs the first available codesigning certificate and then signs a script:

    $certificate = @(Dir cert:CurrentUserMy -codeSigningCert -recurse)[0]
    Set-AuthenticodeSignature c:scriptstest.ps1 $certificate
    
    

    Likewise, to sign all PowerShell scripts on a drive, use this approach:

    Dir C: -filter *.ps1 -recurse -erroraction SilentlyContinue | 
    Set-AuthenticodeSignature -cert $certificate

    When you look at the signed scripts, you'll see a new comment block at the end of a script.

    Attention:

    You cannot sign script files that are smaller than 4 Bytes, or that are saved with Big Endian Unicode. Unfortunately, the builtin script editor ISE uses just that encoding scheme to save scripts, so you may not be able to sign scripts created with ISE unless you save the scripts with a different encoding.

    Checking Scripts

    To check all of your scripts manually and find out whether someone has tampered with them, use Get-AuthenticodeSignature:

    Dir C: -filter *.ps1 -recurse -erroraction SilentlyContinue | Get-AuthenticodeSignature
    

    If you want to find only scripts that are potentially malicious, whose contents have been tampered with since they were signed (HashMismatch), or whose signature comes from an untrusted certificate (UnknownError), use Where-Object to filter your results:

    dir c: -filter *.ps1 -recurse -erroraction silentlycontinue | Get-AuthenticodeSignature | 
    Where-Object { 'HashMismatch', 'NotSigned', 'UnknownError' -contains $_.Status }

    Status Message Description
    NotSigned The file "xyz" is not digitally signed. The script will not execute on the system. Please see "get-help about_signing" for more details. Since the file has no digital signature, you must use Set-AuthenticodeSignature to sign the file.
    UnknownError The file "xyz" cannot be loaded. A certificate chain processed, but ended in a root certificate which is not trusted by the trust provider. The used certificate is unknown. Add the certificate publisher to the trusted root certificates authorities store.
    HashMismatch File XXX check this cannot be loaded. The contents of file "…" may have been tampered because the hash of the file does not match the hash stored in the digital signature. The script will not execute on the system. Please see "get-help about_signing" for more details. The file contents were changed. If you changed the contents yourself, resign the file.
    Valid Signature was validated. The file contents match the signature and the signature is valid.

    Table 10.3: Status reports of signature validation and their causes

    Summary

    PowerShell scripts are plain text files with a ".ps1" file extension. They work like batch files and may include any PowerShell statements.

    To run a script, you need to make sure the execution policy setting is allowing the script to execute. By default, the execution policy disables all PowerShell scripts.

    You can run a script from within PowerShell: specify the absolute or relative path name to the script unless the script file is stored in a folder that is part of the "Path" environment variable in which case it is sufficient to specify the script file name.

    By running a script "dot-sourced" (prepending the path by a dot and a space), the script runs in the callers' context. All variables and functions defined in the script will remain intact even once the script finished. This can be useful for debugging scripts, and it is essential for running "library" scripts that define functions you want to use elsewhere.

    To run scripts from outside PowerShell, call powershell.exe and specify the script path. There are additional parameters like -noprofile which ensures that the script runs in a default powershell environment that was not changed by profile scripts.

    Digital signatures ensure that a script comes from a trusted source and has not been tampered with. You can sign scripts and also verify a script signature with Set-AuthenticodeSignature and Get-AuthenticodeSignature.