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
- Scopes: Variable Visibility
- Profile Scripts: Automatic Scripts
- Signing Scripts with Digital Signatures
- Finding Certificates
- Creating/Loading a New Certificates
- Making a Certificate “Trustworthy”
- Signing PowerShell Scripts
- Checking Scripts
- Table 10.3: Status reports of signature validation and their causes
- Summary
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. 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. 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.