Idera nSoftware Compellent

Chapter 10. Scripts

PowerShell scripts function like batch files in the traditional console: scripts are text files that can include any PowerShell code. If you run a PowerShell script, PowerShell will read the instructions in it, and then execute them. As a result, scripts are ideal for complex automation tasks. In this chapter, you'll learn how to create and execute scripts.

PowerShell makes certain requirements mandatory for their execution because scripts can contain potentially dangerous statements. Depending on the security setting and storage location, scripts must have a digital signature or be specified with their absolute or relative path names. These security aspects will also be covered in this chapter.

Topics Covered:

Writing and Starting PowerShell Scripts

A PowerShell script is nothing more than a text file containing PowerShell code. If the text file is executed, PowerShell steps through its statements and executes them. PowerShell scripts work very much like the batch files of older consoles: you can create PowerShell scripts with much the same simplicity you could using batch files.

Using Redirection to Create Scripts

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

' "Hello world" ' > myscript.ps1

But because you must use quotation marks to enclose text, it can be confusing to use quotation marks inside the script code. Or you may like to specify multi-line text. So, using "here-strings" would work better in this example:

@'
"Hello world"
"One more line"
Get-Process
Dir
'@ > myscript.ps1

Here-strings always begin with @' and end with '@. Everything in between is stored as text, including all special characters and line breaks. If you use double instead of single quotation marks, PowerShell will expand all variables in your here-string.

Creating Scripts with an Editor

Considerably more convenient are genuine text editors, such as Notepad. Assign it the task of creating a new file:

Notepad myscript.ps1

Notepad will open and offer to create the myscript.ps1 file. Click Yes. Now you can write your script in Notepad. Just enter the same statements in Notepad that you would otherwise have typed interactively in the console:

"Howdy!"

Then use File/Save to save your script and close the Notepad.

Starting Scripts

While your script was created, it can't be started just like that. If you enter the file name of your script file, you'll get an error message:

myscript.ps1
The term "myscript.ps1" is not recognized as a
cmdlet, function, operable program, or script file.
Verify the term and try again.
At line:1 char:14
+ myscript.ps1 <<<<

It won't work until you specify at least the relative path name of the script, which is .\myscript.ps1:

.\myscript.ps1
Howdy!

Execution restrictions

PowerShell always initially prohibits scripts from running. Whether scripts can be started or not is determined by the execution policy:

.\myscript.ps1
File "C:\Users\Tobias Weltner\myscript.ps1"
cannot be loaded because the execution of scripts
is disabled on this system. Please see "get-help
about_signing" for more details.
At line:1 char:16
+ .\myscript.ps1 <<<<

Only an administrator can change this setting. The Get-ExecutionPolicy cmdlet will tell you the current setting of your execution policy:

Get-ExecutionPolicy
Restricted

If you want to run scripts, choose another setting from Table 10.1 for the execution policy and use Set-ExecutionPolicy to specify it. You just need to change this setting once. PowerShell will make a permanent note of it.

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

Usually, the best "liberal" setting is RemoteSigned because you can run your own locally stored scripts and potentially dangerous scripts downloaded from the Internet are not allowed:

Set-ExecutionPolicy RemoteSigned
.\myscript.ps1
Howdy!

If you want PowerShell to run only those scripts that you approve, you can sign your scripts digitally. You'll find out how to do that at the end of this chapter. The RemoteSigned setting requires that all the scripts you download from the Internet must be signed. If you select AllSigned, this will apply to local scripts as well. Digital or Authenticode signatures are an excellent means for firms to provide a "stamp of quality" for their PowerShell scripts. They allow only verified and authorized scripts while preventing the execution of potentially hazardous scripts from unknown sources.

Invoking Scripts like Commands

To actually invoke scripts just as easily as normal commands—without having to specify relative or absolute paths and the ".psl" file extension—you can employ two simple tricks. The simplest is alias names. You could define a new alias name for invoking a script:

Set-Alias dosomething .\myscript.ps1

You could immediately launch you script by entering the dosomething command:

dosomething
Howdy!

However, this alias would only work in the same directory in which the script is stored because it uses a relative path name. If you store your scripts in fixed locations, you'd better specify an absolute path name or use environment variables. You could put your script in a central directory and in a profile for all users:

md $env:appdata\PSScripts
directory: Microsoft.PowerShell.Core\FileSystem::
C:\Users\Tobias Weltner\AppData\Roaming
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 09.14.2007 10:00 PSScripts

Then copy the script to this directory:

copy myscript.ps1 $env:appdata\PSScripts\myscript.ps1

Now, specify a fixed destination path independently of the current directory:

Set-Alias dosomething $env:appdata\PSScripts\myscript.ps1

Alternatively, you could declare the directory in which your scripts are stored as a trusted location: include this directory in the Windows Path environment variable. All PowerShell scripts in this directory will no longer require you to specify them by using relative or absolute path names. You no longer even have to append the "ps1" file extension in this connection. Try it out:

# Create a directory for your scripts
md c:\PSScripts
Directory: Microsoft.PowerShell.Core\FileSystem::C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 09.14.2007 10:08 PSScripts

# Copy the script under another name to this directory
copy myscript.ps1 c:\PSScripts\myscript.ps1

# Invoking failed:
myscript
The term "myscript" is not recognized as a
cmdlet, function, operable program or script
file. Verify the term and try again.
At line:1 char:4
+ myscript <<<< 100

# Include the directory in the PATH environment variable:
$env:path += "; c:\PSScripts"

# Invoking succeeded:
myscript
Howdy!

Changes to the Windows environment variable can be risky because they also can have an impact outside the PowerShell console. That's why PowerShell always stores changes to the Windows environment variable only temporarily for the current session. The modifications would be revoked after closing and reopening the PowerShell console.

If you want to make permanent changes to environment variables like Path, do it either outside the PowerShell console or, even better, re-define your preferred settings every time PowerShell starts. You could use profile scripts that run automatically when PowerShell starts. You'll learn about them in the next section.

Passing Arguments to Scripts

You can write scripts that interact with the user, who can then pass arguments to a script. That works for scripts just as it does for the functions in the last chapter. Let's look at how you can modify your first simple script so that its output is not an unchangeable text, but a welcome text that a user can modify.

$args Returns All Arguments

Arguments that you pass to a function or a script are located in the $args variable. To get your script to output the text that the user specifies after the script name when the script is invoked, make the appropriate changes to your script. First, load it back in Notepad as in the following example:

notepad myscript.ps1

Now, change your script in the Notepad and replace the lines in it with these:

"Hello, $args!"

Save the change and try out your modified script.

.\myscript.ps1
Hello, !

The script works, but no text in particular was output. That's obvious because you haven't specified any arguments yet. So, try out the script with an argument:

# The argument is integrated into the text:
.\myscript.ps1 Tobias
Hello, Tobias!

It works and everything that you specify after the script name will be passed as an argument to your script.

$args is an Array

The data you specify after your script when it is invoked are called arguments. PowerShell evaluates these arguments and uses spaces to separate each argument from the other, which explains why your script brings together several spaces in succession:

# Spaces separate arguments. Several spaces
# following each other are combined into one:
.\myscript.ps1 This text has a lot of spaces!
Hello, This text has a lot of spaces!!

PowerShell has identified seven separate arguments from the data that follows your script. You'll find them all afterwards in $args, which is in reality an array. If you would like to use spaces in an argument, and avoid PowerShell interpreting them as separators, your argument must be enclosed in quotation marks:

# Text in quotation marks is understood as precisely one argument:
.\myscript.ps1 "This text has a lot of spaces!"
Hello, This text has a lot of spaces!!

Now all the spaces are output.

Accessing Separate Arguments in $args

Because $args is an array, you could also process each element of the array separately. In Chapter 4, you familiarized yourself with arrays so you know now that you can access elements in an array through an index. So, your script might look like this if you'd like to process the first argument:

"Hello, $($args[0])!"

However,$args[0] can no longer be simply integrated into the output text because PowerShell resolves only simple variables. The entire expression must be wrapped in a direct variable $(...). Save your script, and then look at how your script handles your arguments now:

# Arguments are separated by spaces. "Weltner"
# is the second argument and will not be output:
.\myscript.ps1 Tobias Weltner
Hello, Tobias!

# If you'd like to use arguments with spaces,
# put them in quotation marks:
.\myscript.ps1 "Tobias Weltner"
Hello, Tobias Weltner!

Using Parameters in Scripts

While$args is a simple way to pass user data to a script, it's entirely up to you to find out which argument the user specified and in which order. If the user didn't enter his arguments in exactly the same order you anticipated, the script will get muddled and may not interpret his arguments correctly. In addition, the user won't get any feedback from the script telling him which arguments are permitted or required.

In older script languages, a lot of effort was required to validate passed arguments and to allocate them properly. But PowerShell has this option and it works very much like the parameters of the functions in the last chapter. For functions, parameters specified in parentheses after the function name:

function Test($path, $name) {
"The path is: $path"
"The name is: $name"
}

Test "The path" "The name"
The path is: The path
The name is: The name

Test -name "The name" -path "The path"
The path is: The path
The name is: The name

This works exactly the same way for scripts, just that the question arises of where to put the parameters for a script. While functions are always located in a function Name(Parameter) {...} construct, such a framework isn't available for scripts. That's why scripts have to use the param statement.

Let's translate the function with the two parameters $path and $name into a script. Open your script again for processing in Notepad:

notepad myscript.ps1

Now type this code:

param($path, $name)
"The path is: $path"
"The name is: $name"

Save your script and try it out:

.\myscript.ps1 "the path" "the name"
The path is: the path
The name is: the name

.\myscript.ps1 -name "the name" -path "the path"
The path is: the path
The name is: the name

It works: your script responds now just like the function and uses parameters instead of unnamed arguments. The data in parentheses after param exactly match the same data that you put after the function names in parentheses for functions.

Validating Parameters

After reading the last chapter, you should know how to formulate arguments so that PowerShell can verify that they were specified. The next script requires an argument called name and another called age. It establishes exactly which data type is necessary for each argument and also determines that an error message will be output if the argument is not specified:

param([string]$Name=$( `
Throw "Parameter missing: -name Name"),
[int]$age=$( `
Throw "Parameter missing: -age x as number")) `
"Hello $name, you are $age years old!"

Save this script and execute it. If you forget to specify one of the two arguments, or if you specify the parameter with an invalid value, PowerShell will automatically output an appropriate error message:

# Parameter missing:
.\testscript.ps1
Parameter missing: -name Name
At C:\Users\Tobias Weltner\testscript.ps1:1 char:28
+ param([string]$Name=$(Throw <<<< "Parameter
missing: -name Name"), [int]$age=$(Throw
"Parameter missing: -age x as number"))

# Parameter missing:
.\testscript.ps1 -name Tobias
Parameter missing: -age x as number
At C:\Users\Tobias Weltner\testscript.ps1:1 char:80
+ param([string]$Name=$(Throw "Parameter missing:
-name Name"), [int]$age=$(Throw <<<< "Parameter
missing: -age x as number"))

# Parameter value is invalid:
.\testscript.ps1 -name Tobias -age willibald
C:\Users\Tobias Weltner\testscript.ps1 : Cannot
convert value "willibald" to type "System.Int32".
Error: "Input string was not in a correct format."
At line:1 char:37
+ .\testscript.ps1 -name Tobias -age <<<< willibald

# Parameters are okay:
.\testscript.ps1 -name Tobias -age 212
Hello Tobias, you are 212 years old!

Strictly speaking, there are not any difference at all between functions and scripts. The statements in parentheses after the function name are also translated for functions into exactly the same param statement as they are for scripts. You can easily convince yourself that this is the case by typing the Test function in the preceding example and then outputting the source code of the function. You'll see that the function framework has vanished and the param statement is now in the script block:

$function:test
param($path, $name) "The path is: $path"
"The name is: $name"

Scopes: Ranges of Validity in Scripts

To prevent scripts from having unintentional effects on other scripts or their interactive consoles, they are usually executed in isolation. Here, "isolation" means that all the variables and functions you create in a script are valid only inside the script. If you want to remove isolation, you can "dot source" scripts and functions when you invoke them: this just means putting a single dot or period in front of scripts and functions when you call them. If you want to define the scope of each variable or function separately, use the identifiers described in Chapter 3.

PowerShell stores all the variables in the interactive console in the global: area. All the variables that a script creates are stored in the script: area. When a script has completed its work, its script: area is deleted. That's how PowerShell removes only the variables that the script created. Variables that are already there remain untouched because of their location in the global: area.

This raises an interesting question of how scripts handle variables that have already been defined previously. If a variable doesn't exist in the current scope, PowerShell will try to find it in the parent scope. So, if you created a variable called $test in the console, your script would be able to read this variable. However, change is constant in the current scope: if you modify the contents of the $test variable inside your script, PowerShell would create a new variable called $test in the script: area. The result would be two variables: one in the global: area and one in the script: area. Your script would use the new variable because when you invoke $test inside the script, PowerShell will always look in the current scope first and will not proceed to the parent scope until it cannot locate the sought word. The script can read this variable since $test was created in the current scope. Here's a little test script:

"Variable contents: $test"
$test = "modified"
"Variable contents: $test"

Take a look at how the script handles the $test variable:

# Invoke your script:
.\myscript.ps1

# The $test variable was not defined in the
# global area, and so it is empty:
Variable contents:

# Then the script modifies $test and uses its
# own version in the script: area:
Variable contents: modified

# We will now set a value for $test in the
# global area and restart the script
$test = "default"
.\myscript.ps1

# The script finds the value in $test that was
# set outside the script:
Variable contents: default

# The script can change the value of $test,
# so it uses its own version in the script:
Variable contents: modified

# As soon as the script ends, $test regains
# the old value because the script:
$test
default

You are free to specify which variable store (or area) you want to access by typing the requested area in front of the variable name. In this way, it is entirely possible for a script to make permanent variable changes that will continue to exist after the script is ended. To see how that works, change your script as follows and take another look at the results afterwards:

$test = "default"

# Create a script:
@' "default contents: $test";
$global:test = "modified";
"Variable contents: $test" '@ > myscript.ps1

# Execute a script:
.\myscript.ps1
Variable contents: default
Variable contents: modified
$test
modified

You'll find more details on directly accessing the variable store in Chapter 3. However, in practice it's often sufficient to decide whether a script should be generally isolated or not isolated.

#requires: Script Requirements

Scripts may have certain requirements for their execution. Cmdlets are not necessarily limited to just the ones included in PowerShell. Third-party suppliers offer additional cmdlets. For example, if you're using Microsoft Exchange you'll have many additional special Exchange cmdlets at your disposal.

The cmdlets are part of an additional snap-in that Exchange includes. All additional snap-ins are loaded on the basis of an automatically starting profile script by using Add-PSSnapin. Get-PSSnapin can show you which snap-ins are currently in use by your PowerShell console:

get-pssnapin
Name : Microsoft.PowerShell.Core
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains Windows
PowerShell management cmdlets used to manage
components of Windows PowerShell.
Name : Microsoft.PowerShell.Host
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains cmdlets
used by the Windows PowerShell host.
Name : Microsoft.PowerShell.Management
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains management
cmdlets used to manage Windows components.
Name : Microsoft.PowerShell.Security
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains cmdlets to
manage Windows PowerShell security.
Name : Microsoft.PowerShell.Utility
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains utility
Cmdlets used to manipulate data.

If a script uses commands from an additional snap-in, it can indicate that by using the #requires statement. For example, specify a snap-in after #requires that is essential for the script. If the snap-in is missing, PowerShell won't start the script and will instead output an error message. This is how such a script might look:

#requires -PSSnapin Microsoft.PowerShell.Host
#requires -PSSnapin something.unavailable
"It worked"

While the first requirement appears to be met because this snap-in is part of the basic snap-ins, the second required snap-in is missing. If you try to run the script, you'll get an error message informing you why the script couldn't be started:

.\test1.ps1
The script 'test1.ps1' cannot be run because the following
Windows PowerShell snap-ins that are specified by its
"#requires" statements are missing: something.unavailable.
At line:1 char:11
+ .\test1.ps1 <<<<

If you want to lock in a script to a particular version of PowerShell, use the -Version parameter. Scripts that use new V2-specific features will be able to use #requires -Version 2 to specify that they can't be run with the older version.

Note that you are able to use -ShellID to limit execution of scripts to particular PowerShell consoles. -ShellID is a PowerShell console identifier and is located in the $ShellID automatic variable. For the Microsoft console, the tag is Microsoft.PowerShell. Use #requires -ShellID Microsoft.PowerShell if you'd like to ensure that a script may be executed in Microsoft consoles only.

Making Scripts Understandable

Scripts may be as long as you'd like but typically the longer a script is, the harder it will be to read it. For this reason, lengthy scripts use two methods to keep script code understandable:

  • Functions: Consolidate smaller tasks in functions, which not only make code easier to grasp but can also be reused conveniently. Once you've created a function for a certain task, you can use it later in other scripts as well.
  • Libraries: Embed required functions as a library into your script so you won't need to copy your basic functions into every script, inflating them artificially. Your basic functions can remain in a single script while your current script focuses on just the one task it needs to complete.

Using Functions in Scripts

To be able to use a function inside a script, simply insert the function into the script code. Look at a lengthier script to see how this is done. Open Notepad again:

notepad net.ps1

Then enter the following script and save it:

param ([double]$amount = $(Throw "You have to specify a sum."))

$tax = VAT($amount)
$total = $amount + $tax
"{1:C} VAT is payable on the amount of {0:C}: {2:C}" `
-f $amount, $tax, $total

function VAT($net)
{
$factor = 0.19
$net * $factor
}

This script uses param first to define the amount parameter because the script is supposed to calculate the value-added tax payable on a net sum. The script utilizes the strong type specification we saw in Chapter 3 to set the amount parameter as a floating point number (type: double). If the user doesn't name a number, an error message will be generated.

.\net.ps1
You have to specify a sum.
At C:\Users\Tobias Weltner\net.ps1:1 char:33
+ param ([double]$amount = $(Throw <<<<
"You have to specify a sum."))

If you then specify a sum, the script will generate an error message anyway and will return an incorrect result:

.\net.ps1 100
The term "VAT" is not recognized as a cmdlet,
function, operable program, or script file.
Verify the term and try again.
At C:\Users\Tobias Weltner\net.ps1:3 char:15
+ $tax = VAT( <<<< $amount)
VAT is payable on the amount of $100.00: $100.00

Apparently, the script couldn't locate the VAT function. Unlike most other script languages, PowerShell functions that you define inside your script must be at the beginning of your script. The functions come into play only after PowerShell has read and created the functions with PowerShell reading scripts rigidly from top to bottom. So, a correctly written script should look like this:

param ([double]$amount = $(Throw "You have to specify a sum."))

function VAT($net)
{
$factor = 0.19
$net * $factor
}

$tax = VAT($amount)
$total = $amount + $tax

"{1:C} VAT is payable on the amount of {0:C}: {2:C}" `
-f $amount, $tax, $total

After this transposition, the script now works as expected:

.\net.ps1 100
$19.00 VAT is payable on the amount of $100.00: $119.00

Separating Scripts into Work Scripts and Libraries

Genuine scripts developed to solve practical problems usually include many more than just one function. The scripts may become unclear when function definitions pile up at the beginning of scripts. Functions, once you have created, tested, and approved them, should really not be eye-catching. Prevent that by using script libraries. Save your functions in a separate script file for later inclusion in all your scripts. The scripts can then use the functions saved in the file. Try it out. First, create a script library:

notepad calcfunctions.ps1

Then, enter this function in Notepad and save the script:

function VAT($net)
{
$factor = 0.19
$net * $factor
}

After this step, create a work script. The work script shouldn't include any general functions. Instead, it simply loads the functions it requires from the script library.

notepad net.ps1

Enter the following code and save the script:

param ([double]$amount = $(Throw "You have to specify a sum."))

# Functions will be loaded dot sourced from the library:
. .\calcfunctions.ps1

$tax = VAT($amount)
$total = $amount + $tax
"{1:C} VAT is payable on the amount of {0:C}: {2:C}" `
-f $amount, $tax, $total

The work script will execute the script with the functions first, and then this script will create the required functions. Note that all variables and functions that create a script are as a rule "private" and are valid only within the script. That's critical because scripts must not have unintentional effects on each other. However, because in this case you want the library script to affect the work script (namely by creating new functions that will be disclosed in the work script), the "dot sourced" library script will be invoked: a single dot will be placed in front of the invoked script. The dot will cancel script isolation, and all the functions that the library script creates will also be valid in your work script.

This method is enabling you to load any number of script libraries in your work script. The result is that your work script stays clear and concise while the functions in the libraries can be developed separately.

  • Work script: work scripts shouldn't include any general functions, just the code needed to perform current tasks. Required functions are implemented from external scripts. These must be called by using dot sourcing.
  • Library: libraries may contain only function definitions and no code except for function definitions because the code would otherwise be immediately executed when the library is reloaded.

Library Scripts Central Directory

You should work out a strategy for optimal storage of library scripts as soon as you start using library scripts in your work scripts. One option would be to store the library scripts in the same directory as the work scripts. That way your work scripts could always access its library scripts over a relative path specification, as seen in the preceding example. Another option would be to copy your work scripts to another location, but in then you would have to remember to copy your library scripts as well.

If you'd prefer to store your library scripts in a central location because you want to deny general access to them, or because the library scripts should be stored in a directory that grants users authorization to read them only, and then type absolute path names in your work script. Utilize environment variables to specify the directory where the library script is. If library scripts are in a user profile in the PSLib directory, you could modify your work script in the following way:

param ([double]$amount = $(Throw "You have to specify a sum."))

# Functions will be loaded dot sourced from the library:
. $env:appdata\PSLib\calcfunctions.ps1

$tax = VAT($amount)
$total = $amount + $tax
"{1:C} VAT is payable on the amount of {0:C}: {2:C}" `
-f $amount, $tax, $total

You could use the following lines to create the PSLib directory in a user profile and to copy your locally stored library in this area to which all users could have access:

# Create a directory for commonly used script libraries:
md $env:appdata\PSLib
directory: Microsoft.PowerShell.Core\FileSystem::
C:\Users\Tobias Weltner\AppData\Roaming
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 14.09.2007 09:42 PSLib

# Copy the locally stored library to the central directory:
copy calcfunctions.ps1 $env:appdata\PSLib\calcfunctions.ps1

Use the $MyInvocation automatic variable if you want to know where a script is stored from within a script, to give library scripts an absolute path name that are located in the same directory. Here's an example of a script, which when executed states its name and the directory where it is located:

function get-scriptname
{
if ($myInvocation.ScriptName) { $myInvocation.ScriptName }
else { $myInvocation.MyCommand.Definition; "second" }
}

$myPath = get-scriptname
$myPath
$myParent = split-path $myPath
$myParent

Creating Pipeline Scripts

PowerShell scripts can be used as building blocks in the pipeline just like functions, which were covered in the last chapter. Moreover, what applies to functions applies to scripts as well: depending on how you program the script, you can either force the pipeline to adopt the slow and memory-intensive sequential mode or enable the rapid streaming mode.

Slow Sequential Mode

If you use a script inside the pipeline, the script will collect the results of the preceding statement in the $input automatic variable. But the script also blocks up the pipeline because the pipeline has to wait first until the preceding statement has fully completed its task. Only then can the pipeline pass on its result in $input to the script. Try out a new test script:

notepad filter.ps1

Type this code:

Foreach ($element in $input)
{
If ($element.name.contains(".exe"))
{
Write-Host -fore "red" $element
}
Else
{
Write-Host $element
}
}

The script will read the results in $input and mark every line in red that includes the ".exe" term. The script functions flawlessly in the pipeline:

Dir $env:windir | .\filter.ps1

Long waiting periods occur because the script doesn't go into action until Dir has done its work. Moreover, because all the results of the preceding statement have to be stored temporarily first, memory consumption is extremely high and may even make Windows unstable:

Dir c:\ -recurse | .\filter.ps1

Quicker Streaming Mode

There are reasons why scripts used in the pipeline should support the rapid streaming mode. It works for scripts just as it does for functions: all you need to do is to define the begin, process, and end script blocks in your script. The code in the begin block will be executed once at the beginning and can carry out initialization tasks or output messages to the user. The code in the process block will be executed in real time for every incoming result of the preceding statement, and the code in the end block will be executed at the end. It could carry out cleanup chores or simply report that the operation is concluded. Your filter script would function in real time as follows:

begin
{
"Evaluation is beginning... one moment, please."
}

process
{
if ($_.name.contains(".exe"))
{
Write-Host -fore "Red" $_
}
else
{
Write-Host $_
}
}

end
{
"Evaluation is concluded."
}

A normal script that doesn't implement any of the begin, process or end blocks will automatically get what amounts to an end block. However, you can't combine them. As soon as you insert one of the begin, process or end blocks in your script, no more script code may be left outside one of these blocks. If you do, you will receive an error message like this one:

No combined Begin/Process/End clauses with command
text could be processed. A script or a function can
decide over begin/process/end clauses or command
text, but not over both.
At C:\Users\Tobias Weltner\filter.ps1:23 char:9
+ "Done!" <<<<

Writing Pipeline Results

While your script did process the results of the preceding statement faultlessly, it ended the pipeline. That's wasn't as noticeable because no additional statements followed after your script in the pipeline. The reason: your script received the results of the preceding statement, processed them, and then used Write-Host to write them directly in the console. The results were therefore not passed on in the pipeline. That's OK if your script concludes the pipeline.

However, if you want to write an authentic pipeline script that not only receives pipeline data but hands them on to the next statement, you need to make sure that the processed data are subsequently put back into the pipeline. The next script does that by using a Switch condition to validate a number of file extensions. In this specific example, changing the characters of the names to upper case works:

begin
{
Write-Host " Evaluation is beginning... one moment, please."
}

process
{
$element = $_

Switch($_.Extension.toLower())
{
".ps1" { $element.name.toUpper() }
".vbs" { $element.name.toUpper() }
".txt" { $element.name.toUpper() }
".xml" { $element.name.toUpper() }
default { $element.name.toLower() }
}
}

end
{
Write-Host "Evaluation is concluded."
}

This script simply outputs the results in the process block to the pipeline. In this way, it allows the other following statements to process the results. For this reason, you could use Out-File afterwards to wrap the results in a text file:

Dir | .\filter.ps1 | Out-File list.txt
.\list.txt

Perhaps you noticed that the list includes only the file names and not the start and end messages of the script. As in the preceding example, Write-Host output these two messages. But Write-Host didn't output the file names. That shows the importance of the role Write-Host plays: use it for all messages that are supposed to appear immediately and never be redirected.

Profile: Autostart Scripts

Many changes you make in the PowerShell console are in effect for just a limited period of time. All alias definitions, functions, and changes to Windows environment variables are valid only until you close the PowerShell console. That's why you should use profiles to make basic changes permanent. Profiles are special scripts that PowerShell runs automatically when you start it. Locate all your initialization tasks in profiles so that PowerShell will always use exactly the configuration you want it to use when it starts.

Four Different Profile Scripts

On the whole, PowerShell supports four different profile scripts, which enable you to select a profile that fits your initialization tasks. The first question to ask is: should the initialization tasks apply to you personally or to all users? If you'd like the script to apply to you personally, use your own "current user" profile. However, if your statements are supposed to run for all users whenever PowerShell is started, the correct profile to use is "all users."

Profile Description Location
All users Common profile for all users $pshome\profile.ps1
All users (private) Common profile for all users; valid only in powershell.exe $pshome\Microsoft.PowerShell_profile.ps1
Current user Current user profile $((Split-Path $profile -Parent) + "\profile.ps1")
Current user (private) Current user profile; valid only in powershell.exe $profile

Table 10.2: PowerShell profiles

There are also "private" options for these two profiles. These only function if you use the Microsoft Windows PowerShell console. Are there others? Indeed there are. More and more companies are supporting PowerShell. Alternative consoles already exist that you could use instead of powershell.exe. Use the general profile if you'd like to have your modifications executed when PowerShell starts with applications developed by other companies. Use the private profile If you want your modifications to be executed only when using the original PowerShell console.

Table 10.2 lists the four PowerShell profiles and also tells you where each profile can be found. You might notice a PowerShell design weakness here: the private profile for the current user can be accessed easily by using the predefined variable $profile. That could lead to many users (and add-on developers) stored their extensions in this profile. However, because it is a private profile, it can only be run by the original Microsoft console. And that could become a problem right away if you switch to another company's PowerShell product.

For this reason, you should try not to use private profiles as much as possible so that you can be prepared for future developments. Use the general profile instead, even if it isn't quite so easy to control.

Creating Your Own Profile

Profiles aren't mandatory. That's why you might not have an available profile. You've already seen how easy it is create new PowerShell scripts and it's just as easy to create profile scripts

Perhaps you've created some useful alias shortcuts and would like these alias definitions to be automatically activated whenever PowerShell starts. You can achieve that by creating your own personal profile:

notepad $((Split-Path $profile -Parent) + "\profile.ps1")

This opens Notepad. It will show whether a profile already exists. If not, it will offer to create a new, empty script. Click Yes.

If a profile script already exists, you should inspect it first. In all probability it originates from some PowerShell extension or other that you downloaded and installed. In this case, simply add some statements to the script. For example, insert the following statement into the profile script to set up a new alias called edit, which will allow you to start Notepad conveniently:

Set-Alias edit notepad.exe

Save the script after that, and then close and reopen the PowerShell console. Your profile script will run invisibly in the background, and your new alias command will be created automatically. You can use it for convenient editing of PowerShell scripts:

edit $((Split-Path $profile -Parent) + "\profile.ps1")

Create a Global Profile for All Users

You can create global profile scripts that all system users can use just as easily by specifying the location of the "all users" profile:

notepad $pshome\profile.ps1

Note that this is permitted only if you have administrator privileges. Otherwise, any user could manipulate the start of another user's console. If you aren't the administrator, Windows will deny you permission to store anything at this location.

Windows Vista is a special case. Even when you log on as administrator, Vista will deprive you of administrator status: you're a normal user, at least if you haven't turned off the User Account Control (UAC). Consequently, to create or work on a global profile, you must first activate your full administrator privileges. To accomplish that, don't start PowerShell by clicking in the normal way. Instead, use the right mouse button, and then select Run as Administrator in the shortcut menu. Your PowerShell console will run with full administrator privileges for all programs that you call from within the console, including the Notepad, which you could then use to modify the global profile.

Some editors issue no problem reports if you work on the global profile without administrator privileges. Apparently, these editors have the capacity to make changes to the global profile, as shown by the fact that these changes go into effect when you restart PowerShell—and yet that's not security vulnerability. In reality, Windows Vista just deceives such editors. For compatibility reasons, Vista re-directs the attempt to modify the protected profile file to a hidden shadow area where editors can change profile files in whatever way they wish.

When PowerShell starts again the same thing happens: PowerShell processes the concealed shadow file instead of the protected profile file when no authentic global file is available. Could someone take advantage of this to foist a start script onto other users?

No, there's no risk. The hidden file actually exists only in the user profile of the user who created it. It has no effect on other users and basically behaves exactly like the user's private profile file. You can then find shadow file copies in this directory:

Cd $env:localappdata\VirtualStore

The shadow copy of the global profile is located in the Windows\System32\WindowsPowerShell\v1.0 subdirectory.

Digital Signatures for Your Scripts

Scripts can be easily faked or modified since they are simple text files. . Digital signatures provide greater security because they confirm the identity of the script author and guarantee that the script has not been altered since it was signed. To the extent that you trust the script publisher, you can be sure that nobody is trying to palm off malicious code on you. Even many experts don't completely understand how that works because these mechanisms are based on wickedly complex theories. Fortunately, in the practical world you need not concern yourself about these theories. What's important is that you familiarize yourself with the mechanisms and procedures. For this reason, all the important steps involved in using signatures will be embedded in easily understandable examples in the following sections.

Finding an Appropriate Certificate

Since it is hardly possible to use a classic fountain pen to sign PowerShell scripts (not to mention all other digital data), you'll need another instrument: a certificate as well as a private and secret key. The certificate is your electronic identity and is proof of who produced the signature. The private and secret key ensures that only the certificate owner can use the certificate to produce signatures.

So you're going to need a suitable certificate before you can digitally sign your own PowerShell scripts. The intended "code signing" purpose must be entered into the certificate and you'll also need a private and secret key for the certificate. PowerShell can find out whether certificates that meet these criteria are available on your computer system because all certificates are located in the cert: virtual drive:

Dir cert: -Recurse -codeSigningCert
directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\My
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.

In this case, just one certificate was found, but it could have been more or even none at all. If you have exactly one personal code-signing certificate, you could access it over this line:

Dir cert:\CurrentUser\My -codeSigningCert

What is the difference between Dir cert:\CurrentUser\My and Dir cert:CurrentUser\My? The answer: the first path specification is absolute and consequently always works no matter what your current directory. The second path specification is relative and will go amiss if you have set your current directory to a subdirectory of the certificate store. For this reason, always type a "\" character after cert:.

If you have a choice of several certificates, you have to narrow your choices down to one. In this example, you would specify the certificate name as your choice:

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

You can even use SelectFromCollection() to open an option dialog and easily select a certificate provided that you address the internal functions of the .NET framework from within PowerShell. But first you would have to use LoadWithPartialName() to load the System.Security.dll in advance:

$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

Figure 10.1: Using an option dialog to select a certificate

Creating a New Certificate

In most cases, you won't find any code-signing certificates on your computer so you'll have to obtain one from one of several sources:

  • Private: Companies that run their own Public Key Infrastructure (PKI) will provide you with a private PKI. Typically, only business firms that have their own computing centers or universities can offer this option because a PKI is complex and expensive. Moreover, such certificates are usually valid within their own sphere of influence only.
  • Purchased: Well-known and recognized certification companies like VeriSign or Thawte will be happy to sell you code-signing certificates in return for payment. You won't need your own private PKI, and such certificates are valid worldwide. However, the transaction is expensive and must usually be repeated regularly, such as every year. In addition, you have to go through elaborate procedures to prove your identity to the certifying enterprise.
  • Self-signed:An individual certificate basically requires no complicated PKI. You can simply act as your own signing authority and issue one to yourself. You can then test and tinker with all aspects of the digital signature. Nobody will prevent you from using your own self-signed certificates productively. However, self-signed certificates are not managed by any certifying authorities. You are solely responsible for these certificates and their integrity. If a self-signed certificate lands in the wrong hands, nobody will be able to help you limit damages. For this reason, self-signed certificates are mostly used solely in testing environments and later replaced with certificates issued by a recognized PKI signing authority.

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 athttp://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:programfiles\Microsoft Visual Studio 8\SDK\v2.0\Bin"
.\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 \CurrentUser\My certificate store. From this location, you can now call and use any other certificate:

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

Examining the Code-Signing Certificate

The code-signing certificate represents your digital identity. But let's first take a look at what the certificate "knows" about you. To do so, first call the certificate and store it in a variable:

# Call all code-signing certificates and store them in a field:
$certs = @(Dir cert:CurrentUser\My -codeSigningCert)
"{0} certificates were found." -f $certs.count
3 certificates were found.

# Use the first certificate that was found:
$certificate = $certs[0]

# Who is represented by this certificate?
$certificate.subject
CN=PowerShellTestCert

# Who issued this certificate?
$certificate.issuer
CN=PowerShellTestCert

Declaring a Certificate "Trusted"

As you will quickly see, the certificate naturally contains only data than you specified yourself when creating the certificate. Even falsehoods are allowed. Nobody is going to prevent you from assuming someone else's identity when you create a certificate yourself. This means that certificates are not tamper-proof. The certificate itself "knows" this: if you use Verify() to check whether you can trust the data given in the certificate, PowerShell will respond with False in the case of self-signed certificates: the certificate is not trusted.

$certificate.Verify()
False

And why is the certificate untrustworthy? You can use a little trick to find out the answer. PowerShell can access the options of the System.Security.dll library of the .NET framework to get DisplayCertificate() to display all the data about the certificate in a clearly understandable dialog box. But first you'll have to use LoadWithPartialName() to reload the library:

# Show all the certificate data in a dialog box:
[System.Reflection.Assembly]::`
LoadWithPartialName("System.Security")
[System.Security.Cryptography.x509Certificates.X509Certificate2UI]::`
DisplayCertificate($certificate)

The dialog box tells you what's wrong with the certificate: "This CA Root certificate is not to be trusted. To enable trust, install this certificate in the Trusted Root Certificates Authorities store." In the area below this, the dialog box reports that issued by and issued for are identical, meaning that this is a self-signed certificate not issued by any external PKI. To make this certificate trusted, it must be stored additionally in the certificate store of trusted root certification authorities.

Figure 10.2: Certificates must be declared trusted

In the case of certificates issued by a PKI, there is a difference between the references to issued by and issued for: after issued by, you'll find the name of the signing authority. This is precisely the advantage of PKI: all you need to do is to copy the signing authority just once to the store of trusted root certificate authorities. From then on, all certificates issued by this authority are automatically accepted as trusted. You can use their certificates immediately because the most important commercial certificate authorities are already registered in the store of root certificate authorities and they are valid as a standard of trust.

You can get this done either manually or by letting PowerShell do it for you. The following lines will copy the certificate in $certificate to the store of root certificate authorities:

$Store = New-Object `
system.security.cryptography.X509Certificates.x509Store( `
"root", "CurrentUser")
$Store.Open("ReadWrite")
$Store.Add($certificate)
$Store.Close()

Whenever you put new certificates into the certificate store, a dialog box will ask you whether you really want to do it. This prevents you from running this script unsupervised.

The certificate is immediately trusted; you can use Verify() to check it, and the result will now be True:

$certificate.Verify()
True

If you open the certificate properties again in the dialog box, this will also tell you that the certificate is acceptable. Click the Certification Path tab, and you will see the enabled trust. For self-signed certificates, it is the certificate itself. For certificates issued by a PKI, you will see which signing authority certifies that the certificate on your computer is trusted. The uppermost certificate in this view is always in your store of trusted root certificate authorities.

Figure 10.3: The trusted certificate may now be used for signatures

To find out what exactly happened and how to also perform this procedure manually, take a look at your certificate store:

certmgr.msc

Microsoft Management Console (MMC) opens and shows you your certificate store. In the Personal Certificates\Certificates branch, you'll find all your personal certificates, including the code-signing certificates that you created yourself, so you'll also find a copy of your self-signed certificate. If you delete the certificate, it will no longer be trusted. If you use your right mouse button to drag your self-signed certificate from the Personal Certificates\Certificates branch to the Trusted Root Certification Authorities\Certificates branch, you will only need to select Copy here to carry out the same copy procedure that your PowerShell code just automated for you.

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.

Using the First Available Certificate

Dir along with the parameter -codeSigningCert will retrieve appropriate code-signing certificates. In the most rudimentary case, you can use the first available certificate to sign one—or even all—PowerShell scripts in your current directory. The following lines will create a simple PowerShell script named test.ps1 and sign this script file with first available code-signing certificate:

' "Hello world" ' > test.ps1
$certificate = @(Dir cert:CurrentUser\My `
-codeSigningCert -recurse)[0]
Set-AuthenticodeSignature test.ps1 $certificate
directory: C:\Users\Tobias Weltner

SignerCertificate Status Path
----------------- ------ ----
E24D967BE9519595D7D1AC527B6449455F949C77 Valid test.ps1

The signature will be directly inserted into the script as a data block and consist of a digital fingerprint of the script (also known as a hash), which can be encrypted using the private key of the certificate. You'll find out how useful this is in the next section.

# Disclose the signature in the script file:
type test.ps1
"Hello world"

# SIG # Begin signature block
# MIIEEQYJKoZIhvcNAQcCoIIEAjCCA/4CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUf02ePVE/w2QMUVYbQhkeTsl4
# AdqgggIqMIICJjCCAY+gAwIBAgIQ0+Yc503n6LJKxel1bq1xtTANBgkqhkiG9w0B
# AQQFADAdMRswGQYDVQQDExJQb3dlclNoZWxsVGVzdENlcnQwHhcNMDcwOTE0MTAz
# MTE0WhcNMzkxMjMxMjM1OTU5WjAdMRswGQYDVQQDExJQb3dlclNoZWxsVGVzdENl
# cnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAO99s+DoANjTbcx1AYfvlR0q
# MnoWKkHm9oc+F8hLAXpI8fPiBnxlqrwhZcmiuE1dE1rYIFktomNNtS0i70G2d445
# o5mUKRtZ9THuwYGnCY+luDBM5cmN0sjcJK9iPHGgtIjFylYwMXhgHA8bBODc8zf0
# 54lSoH5NTOB7uZ4fijVfAgMBAAGjZzBlMBMGA1UdJQQMMAoGCCsGAQUFBwMDME4G
# A1UdAQRHMEWAEAtDyFc0PeNlfKpgXP1kDKahHzAdMRswGQYDVQQDExJQb3dlclNo
# ZWxsVGVzdENlcnSCENPmHOdN5+iySsXpdW6tcbUwDQYJKoZIhvcNAQEEBQADgYEA
# lkCaA6rqq9f/RJifhLY3gZPABVtymP6SGbm6LgASLKzYfdhcmsDxOnwQjAzo4xDk
# nLux4JccT9vFM+0tR/5d3alsY9rH8E+y8gs6opZNsg0ls4CCDrEWCMD3BOk70ch5
# yVCv0PDqtLboO/O4dcJiGt9HViUNISHMEYnlR1qgBJExggFRMIIBTQIBATAxMB0x
# GzAZBgNVBAMTElBvd2VyU2hlbGxUZXN0Q2VydAIQ0+Yc503n6LJKxel1bq1xtTAJ
# BgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0B
# CQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAj
# BgkqhkiG9w0BCQQxFgQUwY+7iwxEhe2RiHMICRnV/mGny5gwDQYJKoZIhvcNAQEB
# BQAEgYAyscnxSQsTeqIkmh92ros8NBS+L7tvwRDl8KwAwvBVsMTy7cFzz3lnqc5T
# /25KFjcVp0Id6oKsQgHW07zdlcR7mC9nfwSKPBTE2G1+tmLHNopMqlcwjH0YriBW
# f25oYXEKRMMgzsuwC4IjblrVGBe+MdcJy1Cmd2qR3UQXm3m6ZA==
# SIG # End signature block

Recursively Signing All PowerShell Scripts

Set-AuthenticodeSignature allows you to sign not only individual scripts, but also many scripts at once in one operation. That means you could use a few lines to sign all your personal PowerShell scripts with your digital signature. Set-AuthenticodeSignature will also accept arrays as file names that can contain any number of separate file names. Instead of a fixed file name, enclose in parentheses a subexpression in your statement, and let Dir list all the PowerShell scripts in the current directory. They will all be signed immediately.

$certificate = @(Dir cert:CurrentUser\My -codeSigningCert -recurse)[0]
Set-AuthenticodeSignature (Dir *.ps1) $certificate
SignerCertificate Status Path
----------------- ------ ----
E24D967BE9519595D7D1AC527B6449455F949C77 Valid filter.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid myscript.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid net.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid calcfunctions.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid test.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid test1.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid test3.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid testscript.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid unsigned.ps1

If you'd like to sign the scripts in your current directory as well as all PowerShell scripts in all subdirectories, the invocation is just as clear because you only need to use the -recurse parameter:

Set-AuthenticodeSignature (Dir -recurse -include *.ps1) $certificate

Selecting Certificates Using the Dialog Box

If there's more than one code-signing certificate on your computer, such as certificates used for diverse purposes, then you surely wouldn't want to use the first available certificate but the most suitable one. One option is to use the certificate name if you know it:

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

Another option is to use the built-in dialog box of .NET framework. It lists all certificates for selection that you pass to SelectFromCollection(). Before you can do this, you must wrap the certificates in a special collection. In the simplest scenario, you should offer all code-signing certificates for selection:

# Text for the dialog box:
$title = "Available identities"
$text = "Please select a certificate for signing"

# Find certificates:
$certificates = Dir cert:\ -recurse -codeSigningCert

# Load System.Security librara and wrap
# certificates in a collection:
[System.Reflection.Assembly]::`
LoadWithPartialName("System.Security")
$collection = New-Object `
System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$certificates | ForEach-Object { $collection.Add($_) }

# Display options:
$certificate = `
[System.Security.Cryptography.x509Certificates.X509Certificate2UI]::`
SelectFromCollection($collection, $title, $text, 0)

# Use selected certificate to sign
Set-AuthenticodeSignature -Certificate $certificate[0] `
-FilePath test.ps1
directory: C:\Users\Tobias Weltner
SignerCertificate Status Path
----------------- ------ ----
372883FA3B386F72BCE5F475180CE938CE1B8674 Valid test.ps1

Validating Signed PowerShell Scripts

How exactly do signatures in scripts benefit you and others? The simple answer is they can be validated, both manually and automatically, and tell you whether a PowerShell is trusted or may contain malicious code.

  • Validate it yourself: For manual validation, check whether a signature is in a PowerShell script and if it is, whether it is unobjectionable. Among other things, you can find out who signed the script, whether the script code was changed, and whether whoever signed the script is someone you trust.
  • Validate automatically: If you set the PowerShell execution policy to AllSigned, PowerShell will carry out validation automatically as soon as you attempt to run the script. The script will run only if the script issuer is trusted and the signature has not been altered since it was signed.

Manual Validation

The cmdlet Get-AuthenticodeSignature validates signatures. This cmdlet requires the name of the script file that you want to examine. The script file doesn't have to include a signature. Whether it does or doesn't, the StatusMessage property will tell you the script status:

' "Hello" ' > unsigned.ps1
$check = Get-AuthenticodeSignature unsigned.ps1
$check.StatusMessage
The file "C:\Users\Tobias Weltner\unsigned.ps1"
is not digitally signed. The script will not execute
on the system. Please see "get-help about_signing"
for more details.

Note that this text conveys exactly the same message that you would receive if you ran an unsigned script, even though the execution policy is set to RemoteSigned or AllSigned. This means that PowerShell carries out precisely the same validation procedure internally when, depending on the current execution policy, it examines the scripts you try to start. Another equally useful property is Status, which summarizes the script status in just one concise phrase:

$check.Status
NotSigned

What happens when you inspect script signatures? Table 10.3 provides an overview of possible validation results, as well as causes. You can use get-authenticodesignature to easily ascertain the security status of scripts, which scripts have a valid signature, and which scripts lack signatures or whose contents have been modified:

Get-AuthenticodeSignature (Dir *.ps1)
directory: C:\Users\Tobias Weltner

SignerCertificate Status Path
----------------- ------ ----
E24D967BE9519595D7D1AC527B6449455F949C77 Valid filter.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid hauptskript.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid myscript.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid net.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid calcfunctions.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid test.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 HashMismatch test1.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 UnknownError test3.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid testscript.ps1
E24D967BE9519595D7D1AC527B6449455F949C77 Valid unsigned.ps1
NotSigned unterskript.ps1

If you want to see only those 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:

Get-AuthenticodeSignature (Dir *.ps1) |
Where-Object {(($_.Status -eq "HashMismatch") `
-or ($_.Status -eq "UnknownError"))}
directory: C:\Users\Tobias Weltner

SignerCertificate Status Path
----------------- ------ ----
E24D967BE9519595D7D1AC527B6449455F949C77 HashMismatch test1.ps1
94FD1387CE1CA1340E59A7B16541C6179FDEEC7D UnknownError test3.ps1
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

Automatic Validation

You don't need to validate the signatures of your script files because PowerShell will carry out validation automatically when you try to start a script. The script will run only if the script file signature is valid. In all other cases, you will get an error message like those in Table 10.3. In this way, you can ensure that only those scripts will run that were inspected by a trusted authority and were found to be valid (that is, signed). Automatic validation will alert you as well if the script contents have been subsequently modified.

Automatic validation is always active when you use Set-ExecutionPolicy to set the execution policy either to AllSigned or RemoteSigned. All scripts will be tested in principle if you choose AllSigned.

If you set your execution policy to AllSigned, you should make sure that your profile scripts are correctly signed. Otherwise, PowerShell will no longer execute the profile scripts.

If you select RemoteSigned, only those scripts will be checked that you downloaded from the Internet, received as an e-mail attachment, or from some other unreliable source. Here's a little test:

# Set ExecutionPolicy to AllSigned. All
# scripts must now have a valid signature:
Set-ExecutionPolicy AllSigned

# Create an unsigned test script file.
# It will not be able to run:
' "Hello world" ' > test1.ps1
.\test1.ps1
The file "C:\Users\Tobias Weltner\test1.ps1"
cannot be loaded. The file "C:\Users\Tobias
Weltner\test1.ps1" is not digitally signed.
The script will not execute on the system. Please see
"get-help about_signing" for more details.
At line:1 char:11
+ .\test1.ps1 <<<<

# Sign the script file with an untrusted certificate:
$certificate = Dir cert:\CurrentUser\My |
Where-Object { $_.Subject -eq "CN=malicious certificate" }
Set-AuthenticodeSignature test1.ps1 $certificate
directory: C:\Users\Tobias Weltner

SignerCertificate Status Path
----------------- ------ ----
94FD1387CE1CA1340E59A7B16541C6179FDEEC7D Valid test1.ps1

# If the certificate is not trusted,
# you will always get an error message:
.\test1.ps1
The file "C:\Users\Tobias Weltner\test1.ps1"
cannot be loaded. A certificate chain processed,
but ended in a root certificate which is not
trusted by the trust provider.
At line:1 char:11
+ .\test1.ps1 <<<<

# Sign the script with a trusted certificate:
$certificate = Dir cert:\CurrentUser\My |
Where-Object { $_.Subject -eq "CN=PowerShellTestCert" }
Set-AuthenticodeSignature test1.ps1 $certificate
directory: C:\Users\Tobias Weltner

SignerCertificate Status Path
----------------- ------ ----
E24D967BE9519595D7D1AC527B6449455F949C77 Valid test1.ps1

# If you used a trusted certificate for the signature,
# the script will be allowed to run:
.\test1.ps1
Hello world

The sole difference between a trusted and an untrusted certificate is the question of whether the certificate publisher is specified in the special trusted root certificates authorities store. But even when you invoke a script signed with a trusted certificate, your first invocation will be accompanied by an additional query:

Do you want to run software from this untrusted publisher?
The file "C:\Users\Tobias Weltner\testscript.ps1" is published
by "CN=PowerShellTestCert". This publisher is not trusted on
your system. Only run scripts from trusted publishers.
Email Never run No Do not run [M] Run once Angel Always run
[?] Help (default is "N"):

Only when you answer by selecting "A" for "Always run" will the certificate publisher be placed in the trusted root certificates authorities store. Then, you won't be pestered with further queries for all the scripts signed with this certificate. If you'd like to avoid this query from the start, simply add the publisher of your script to the list of trusted root certificates authorities and also to the list of trusted publishers. With self-signed certificates, you could type:

# Select certificate:
$name = "PowerShellTestCert"
$certificate = Dir cert:\CurrentUser\My |
Where-Object { $_.Subject -eq "CN=$name " }

# Declare certificate publisher to be generally trusted
$Store = New-Object `
system.security.cryptography.X509Certificates.x509Store( `
"root", "CurrentUser")
$Store.Open("ReadWrite")
$Store.Add($certificate)
$Store.Close()

# Run certificates of this publisher:
$Store = New-Object `
system.security.cryptography.X509Certificates.x509Store( `
"TrustedPublisher", "CurrentUser")
$Store.Open("ReadWrite")
$Store.Add($certificate)
$Store.Close()

Building a Miniature PKI

You've seen that you can use self-signed certificates to fully utilize PowerShell security functions without an elaborate PKI. While a managed PKI is the better approach, you should also look at how you can build your own miniature PKI with the help of the Microsoft tool makecert.exe. before you decide to entirely forego the security of digital signatures just because you have no PKI available.

In the following example, the intended aim is to allow a business department to sign PowerShell scripts created by you with valid signatures. The signatures are to be valid across the enterprise. In addition, every staff member of the department will receive a personal certificate so that it will be possible to trace who has signed which script.

Creating a Root Certificate

The first step is to create for the department a root certificate, which will not actually be used later for signing purposes. It serves merely as publisher of staff certificates. The root certificate will not be created in the certificate store of the current user but in the Local Machine store so you will require administrator privileges. This is how to create a root certificate:

$departmentname = "IT Department 23"
pushd
Cd "$env:programfiles\Microsoft Visual Studio 8\SDK\v2.0\Bin"
.\makecert -n "CN=$departmentname" -a sha1 -eku 1.3.6.1.5.5.7.3.3 `
-r -sv root.pvk root.cer -ss Root -sr localMachine
Succeeded

Popd

Makecert has created the root certificate as well as the files root.pvk and root.cer. Both will be used later but right now you should verify that the certificate was created properly:

$certificate = Dir cert:\LocalMachine\Root |
Where-Object { $_.Subject -eq "CN=$departmentname" }
$certificate
directory: Microsoft.PowerShell.Security\Certificate::
LocalMachine\Root

Thumbprint Subject
---------- -------
AD68EC74428B4F294B1FDF7EB8A64D5ED327F84B CN=IT Department 23

Creating Staff Certificates

With the help of the root certificate, you can now create any number of staff certificates as long as you know the secret password that you stipulated when creating the root certificate. Ideally, only the department head knows the password. This is how you would proceed to create a new staff certificate:

$staff = "Tobias Weltner"
pushd
Cd "$env:programfiles\Microsoft Visual Studio 8\SDK\v2.0\Bin"
.\makecert -pe -n "CN=$staff" -ss MY -a sha1 -eku 1.3.6.1.5.5.7.3.3 `
-iv root.pvk -ic root.cer
Succeeded

popd

Makecert registers the previous root certificate as publisher in the new staff certificate. This information is loaded by makecert from the root.pvk and root.cer files, which were generated when the root certificate was created. You should store these two files in a safe location as soon as all staff certificates have been created. You will need these two files if you want to create additional staff certificates later. Protect in particular the root.pvk file from unauthorized access, because whoever has this file (as well as the secret access code you invented when you created the root certificates) can make new staff certificates.

Verify that the staff certificate was created properly:

$staff = "Tobias Weltner"
$certificate = Dir cert:\CurrentUser\My |
Where-Object { $_.Subject -eq "CN=$staff" }
[System.Reflection.Assembly]::`
LoadWithPartialName("System.Security")
[System.Security.Cryptography.x509Certificates.X509Certificate2UI]::`
DisplayCertificate($certificate)

The dialog box will now show differing specifications for Issued by and Issued for. The issuer of the staff certificate is now your new root certificate, and if you click the Certification path tab, you'll then see a genuine chain of trust starting with the root certificate for your department. That has great advantages because now all that remains to be done is to register your root certificate in the store of trusted root certification authorities in the entire enterprise. All the staff certificates originating from your root certificate are now automatically trusted.

Creating a Backup

Every staff member should save a copy of his staff certificate and store it in a protected location. The backup can be done directly from within PowerShell. The following lines will create a password-protected PFX file with the name backup.pfx in the current directory. In the example, the password is set to "strictlyconfidential" and, of course, should never be modified. The certificate, along with its secret and private key can be imported again only if the specified password is known.

$filename = "$(get-location)\backup.pfx"
$pwd = "strictlyconfidential"

[System.Reflection.Assembly]::LoadWithPartialName("System.Security")
$collection = New-Object `
System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$collection.Add($certificate)

$bytes = $collection.Export(3, $pwd)
$filestream = New-Object System.IO.FileStream($filename, "Create")
$filestream.Write($bytes, 0, $bytes.Length)
$filestream.Close()

If you assume the role of department head, you can now create a code-signing certificate for every staff member, generate a respective pfx backup copy, and then forward this to every staff member. To open this pfx file, a staff member would only need to double-click the file, enter the assigned password, and confirm all further settings. Finally, the certificate could be installed in its own certificate store, and staff members could begin to sign their scripts.

Installing Enterprise-Wide Root Certificates

Your "miniature PKI" should already be functioning on the computer where you stored the root certificate. So that your new staff certificates are recognized enterprise-wide, register the root certificate across the enterprise in the store of trusted root certification authorities. You can do that either manually or you can use Group Policy guidelines in an Active Directory for automatic distribution.

PowerShell installs the root certificate in the root.cer file in the system-wide store of trusted root certification authorities in the following way:

copy "$env:programfiles\Microsoft Visual Studio 8\SDK\v2.0\Bin\root.cer" `
"root.cer"
$Store = New-Object `
system.security.cryptography.X509Certificates.x509Store( `
"root", "LocalMachine")
$filename = "$(get-location)\root.cer"
$store.Open("ReadWrite")
$collection = New-Object `
System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$collection.Import($filename)
$store.Add($collection[0])
$store.Close()

You could likewise open the root.cer file by double-clicking it or invoke it from within PowerShell:

.\root.cer

In this case, you would install the certificate interactively with the help of an assistant. In the dialog box, click the Install certificate button. Follow the directions of the assistant, and select the option Save all certificates in the following store. Click Search.

A further dialog box should open. Select the option Display physical store. Then select in the upper tree structure the following branch: Trusted root certification authorities/Local computer. Click OK and then Continue to install the certificate.

Summary

PowerShell scripts are text files with a ".psl" file extension. They function like the batch files of older consoles and may include any PowerShell statements. If you start a PowerShell script, PowerShell will execute its included statements.

You can't start scripts without the permission of the execution policy. This setting initially prohibits scripts from starting, but an administrator can use Set-ExecutionPolicy to change the setting (see Table 10.1) and specify which scripts are allowed to start. The execution policy can specify that only those scripts may run that have a valid digital signature; it can also distinguish between local scripts and scripts originating from the Internet.

To execute a PowerShell script, the script must be invoked with its relative or absolute path name. For this reason, it does not suffice to specify only the script name unless the script is in a trusted directory, meaning all directories that are named in the Path environment variable. Another way to launch scripts comfortably is to use an alias name that you assign to the script with the help of Set-Alias.

Arguments can be passed to scripts. PowerShell automatically analyzes all the data that you specify after a script name when you invoke a script, and it uses a space as separator for arguments. The arguments are provided to the script in $args. Alternatively, the script can also bind arguments to set parameters. To do so, the parameters, much like functions, are defined inside the script by using the Param statement.

So to ensure that elaborate scripts remain clearly understandable, individual tasks should be encapsulated as functions. Functions must always be located at the beginning of a script. However, they can be relocated to an external library script that is subsequently reloaded by a work script similar to an Include statement.

PowerShell scripts may be used inside the pipeline. So that scripts do not block the pipeline, they must, like functions, define at least one process block. The block is separately invoked for every object in the pipeline.

All variables and functions that a script creates are private and apply only within the script. If you want to cancel their isolation, carry out a dot-sourced invocation of scripts and functions, such as typing a single dot in front of them when they are called. Set the validity of separate variable and function layers by using area designators like script: and global:.

When starting, PowerShell automatically looks for a series of profile scripts. If they are present, PowerShell runs them automatically provided that the execution policy allows their execution. You can set up the PowerShell work environment in the profile scripts and define alias names or functions that are to be provided automatically after PowerShell starts.

Digital signatures ensure that a script originates from a trusted source and has not been subsequently modified. You can imagine such scripts as a stamp of quality. Depending on the execution policy setting, PowerShell will permit only those scripts to run that have this stamp of quality.


Posted Mar 30 2009, 08:01 AM by ps1

Comments

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 9. Functions
on 04-01-2009 2:31 PM

PowerShell has the purpose of solving problems, and the smallest tool it comes equipped with for this is commands. By now you should be able to appreciate the great diversity of the PowerShell command repertoire: in the first two chapters, you already

Chapter 10. Scripts - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com wrote Chapter 10. Scripts - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com
on 04-01-2009 2:32 PM

Pingback from  Chapter 10. Scripts - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 12. Command Discovery and Scriptblocks
on 04-01-2009 2:33 PM

In previous chapters you learned step by step how to use various PowerShell command types and mechanisms. After 11 chapters, we have reached the end of the list. You'll now put together everything you've seen. All of it can actually be reduced

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 16. The Registry
on 04-01-2009 2:35 PM

You can navigate the Windows registry just as you would the file system because PowerShell treats the file system concept discussed in Chapter 15 as a prototype for all hierarchical information systems.

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 20. Your Own Cmdlets and Extensions
on 04-10-2009 5:41 PM

Since PowerShell is layered on the .NET framework, you already know from Chapter 6 how you can use .NET code in PowerShell to make up for missing functions. In this chapter, we'll take up this idea once again. You'll learn about the options PowerShell

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 1. The PowerShell Console
on 04-30-2009 12:50 AM

Welcome to PowerShell! This chapter will teach you about all aspects of the PowerShell console from A to Z. You'll also learn how to configure the console to suit your personal preferences including font colors and sizes, editing and display options

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 2. Interactive PowerShell
on 04-30-2009 12:51 AM

PowerShell has two faces: interactivity and script automation. In this chapter, you will first learn how to work with PowerShell interactively. Then, we will take a look at PowerShell scripts.

Chapter 3. Variables - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com wrote Chapter 3. Variables - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com
on 04-30-2009 12:52 AM

Pingback from  Chapter 3. Variables - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com

Master-PowerShell | With Dr. Tobias Weltner wrote Chapter 5. The PowerShell Pipeline
on 04-30-2009 12:54 AM

The PowerShell pipeline chains together a number of commands similar to a production assembly. So, one command hands over its result to the next, and at the end, you receive the result.

KodefuGuru wrote Free PowerShell EBook
on 07-24-2009 3:24 PM

Free PowerShell EBook

Copyright 2010 PowerShell.com. All rights reserved.