Showing posts with label PowerShell. Show all posts
Showing posts with label PowerShell. Show all posts

Tuesday, 22 January 2013

PowerShell Pitfalls (Non-Fatal Exceptions, Test-Path, Drives, Jobs)

So if you are trying to use PowerShell for some significant real stuff you will run into many pitfalls just like me. So much so I could dedicate a few blog posts just to get some functions out, but in trying to mount a TrueCrypt drive and checking its existence afterwards I ran into many annoying problems. So in C# I would just use Directory.Exists() and be done with it. But the PowerShell way is Test-Path. You can still use the C# version [System.IO.Directory]::Exists() but since using Test-Path and wanting to be more PowerShelly I stuck to it and forgot about Directory.Exists() I mean its the same thing anyway, right? No way! It seems like there are some other checks which I'm not sure what perhaps permissions etc the function tests for both existence and accessibility (permissions). All i really wanted to test was if the drive exists. So one other quick way is to useGet-ChildItem "C:\" and wrap this in a try except and check for exception of type System.Management.Automation.DriveNotFoundException

So much like:


function DriveExists($path)
{ 
 try
 {

  Get-ChildItem $path | Out-Null
 }
 catch
 {
  if ($_.Exception.gettype() -eq [System.Type]::GetType("System.Management.Automation.DriveNotFoundException"))
  {
   Write-Host "FAIL!"
   Write-Host $Error[0].exception
   return $false   
  }  
 }
 return $true
}

But this doesn't work either because the catch block is never executed, WTF? So it turns out that in powershell only fatal exceptions will invoke the catch block: To get around this you have to check the return value from the last command as suggested and then check the error:
   function DriveExists($path)
{ 
 Get-ChildItem $path -ErrorAction SilentlyContinue | Out-Null
 if (-not $?) # http://www.neolisk.com/techblog/powershell-specialcharactersandtokens
 {
  if ($Error[0].exception.gettype() -eq [System.Type]::GetType("System.Management.Automation.DriveNotFoundException"))
  {
   return $false   
  }    
 }  

 return $true
}
Now that I have this handy function again i found that for some reason a new drive mounted while executing the script cannot be seen, again WTF? It's as if it is cached in the session and can't be accessed. Then I think hey let me just test the existence by using Directory.Exists() and what do you know it works. In order to get Test-Path to work I had to run this out of process in a new session using a job, so something like this:
function DriveExistUsingJob($path)
{
 $ScriptBlock = {  Write-Output (Test-Path $args[0]) }
 $Job = Start-Job -ScriptBlock $ScriptBlock -ArgumentList @($path)
 Wait-Job $job | Out-Null
 $jobResult = [string](Receive-Job -Job $Job )
 while ($job.HasMoreData)
 {
  $jobResult = [string](Receive-Job -Job $Job)
 }
 
 return $jobResult
}
So why did I use PowerShell's Test-Path function one in the first place? I don't know I think I'll stick to the C# functions where I can, at least I know what that does. Remember: All I really wanted to do was to say Directory.Exists(), but no... It really makes me feel like Windows Powers Hell or at least mine.

Wednesday, 25 July 2012

PowerShell - Using Credentials

Send-MailMessage

In PowerShell you can use the Send-MailMessage command to simply send an email through SMTP. This is easy enough and the command is simply to call this with some arguments as follows:

Send-MailMessage
1
Send-MailMessage -SmtpServer "mail.yourserver.com" -Subject "Error" -From "user@domain.com" -To "user@domain.com" -Body "test"

The problem is that you may get an authentication error:

The SMTP server requires a secure connection or the client was not authenticated.
The server response was: 5.5.1 Authentication Required.

The issue is that you need to specify a credential by using the -Credential parameter

You can use the Get-Credential Cmdlet, but this will show a prompt unless you specify the username and password in your script file. This is not good for anyone viewing or copying our script file. Turns out we can easily encrypt this information using the ConvertTo-SecureString:

First we need to get the password from user input:
Read-Host
1
Read-Host -AsSecureString
Output
************
System.Security.SecureString
PS C:\>

We can also read it from a UI prompt:

Get-Credential
1
(Get-Credential -Credential "user").Password
Send-MailMessage_1

You can also save this password to a file like this:

Get-Credential
1
(Get-Credential -Credential "user").Password | ConvertFrom-SecureString | Out-File "C:\temp\mycredential.cred"

Which will produce output that looks something like this:

Securestring output
01000000d08c9ddf0115d1118c7a00c04fc297eb0100000064ff74aec2ba6d428b22782a6f91faac000000000200000000001066000000010000200000005b03f7a95e78f996e0a25c5b50fc3f481cf275092131e7fc45eb7553db7d1220000000000e800000000200002000000060b480288abe198159022376076f3f6663478790e547238af5d8550b65a9b6f810000000f536330ee4f0a2a2cd37da4472c4ed17400000008d65be8e08d68ce5472c25f70b60a252d9380b7e950a5780b253e1e20bccbd491bb65d5db6513b0e8925eb6e70f730e21b09223981f43f96dc2b1de12a3407fe

Now these secure strings cannot be decoded by anyone else but your own logged on user, if you try to take this file and decrypt it on a different machine you will get this error:

SecureString error
ConvertTo-SecureString : Key not valid for use in specified state.
 
At line:1 char:58
+ cat ("C:\temp\mycredential.cred") | convertto-securestring <<<<
    + CategoryInfo          : InvalidArgument: (:) [ConvertTo-SecureString], CryptographicException
    + FullyQualifiedErrorId : ImportSecureString_InvalidArgument_CryptographicError,Microsoft.PowerShell.Commands.ConvertToSecureStringCommand

Note: Initially i thought that I could make this more secure by providing an additional security key something like this:

ConvertFrom-SecureString
1
ConvertFrom-SecureString -Key ([System.Text.Encoding]::ASCII.GetBytes("0e5c436f3993432590f12c0b2a48dafa"))

The Key i would use I would simply generate as follows:

NewGuid
1
[System.Guid]::NewGuid().ToString().Replace('-', '')
Output
0e5c436f3993432590f12c0b2a48dafa
PS C:\>

But this actually circumvents the Data Protection API. This page has a good explanation and more information.

So which way is more secure? Well that depends on your needs and environment but I think when the key is in your script file it is easier for an attacker to copy the file and script and then decrypt the information. So as always it is a good idea to apply as many security features as possible and keep the file safe and private, apply permissions and possibly use a different or lower access account.

The simplest way to save the credential to a file would be as follows:

Get-Credential
1
(Get-Credential -Credential "user").Password | ConvertFrom-SecureString | Out-File c:\temp\mycredential.cred
c:\temp\mycredential.cred
01000000d08c9ddf0115d1118c7a00c04fc297eb0100000021edac733db042498f873cbbdc18c766...
And we could save this to a file. However when running this same script say off a flash drive could be a problem when you create the encrypted file and load it again as they will be different for each logged on session. So I thought to create a reusable function that rather takes a store folder and saves a different file for each users unique account which is done by getting the AccountDomainSid:
GetFile
1
2
3
4
5
function GetFile($storeFolder)
{
     return [System.IO.Path]::Combine($storeFolder,
        [System.Security.Principal.WindowsIdentity]::GetCurrent().User.AccountDomainSid.ToString())
}
CreatePassword
1
2
3
4
function CreatePassword($storeFolder)
{
    (Get-Credential -Credential "user").Password | ConvertFrom-SecureString | Out-File (GetFile $storeFolder)
}

And use it:

Output
PS C:\> CreatePassword c:\temp

Creates a file S-X-X-XX-XXXXXXX-XXXXXXX-XXXXXXXXX

c:\temp\S-X-X-XX-XXXXXXX-XXXXXXX-XXXXXXXXX
01000000d08c9ddf0115d1118c7a00c04fc297eb0100000064ff74aec2ba6d428b22782a...

Reading the password:

GetPassword
1
2
3
4
function GetPassword($storeFolder)
{
    return cat ((GetFile $storeFolder)) | ConvertTo-SecureString
}

And use it:

Output
PS C:\> GetPassword c:\temp
System.Security.SecureString

Now as you can see we can't actually see the password because it is securely stored in the System.Security.SecureString But we can use this object to create Credentials.

Creating a credential:

ReadCredential
1
2
3
4
function ReadCredential($userName, $storeFolder)
{
    return new-object -typename System.Management.Automation.PSCredential -argumentlist $userName, (GetPassword $storeFolder)
}

And use it:

Output
PS C:\> ReadCredential "DOMAIN\user" "C:\temp"
 
UserName       Password
--------       --------
DOMAIN\user    System.Security.SecureString
 
PS C:\>

Putting it all together

As always you can create a reusable module from the functions above and use it whenever you need to save or load credentials. Just pick a filename such as CredentialFunctions.psm1 and then import it using:
Import-Module
1
Import-Module  -Name "c:\PowerShell\CredentialFunctions.psm1"

And now you can simply use this in credential functions such as Send-MailMessage

Send-MailMessage
1
Send-MailMessage -SmtpServer "mail.yourserver.com" -Subject "Error" -From "user@domain.com" -To "user@domain.com" -Body "test" -Credential (ReadCredential "DOMAIN\user" "C:\temp")

Powershell has a number of functions that may require Credentials and you could use this to do things like start processes etc.

Final note:

Even though the password is encrypted securely and with a key don't leave the file lying around and still protect it for instance on a flash disk. An attacker could still having access to your machine and your script key decrypt it. Lastly don't abuse the SMTP server.

Saturday, 7 July 2012

PowerShell–Zip

Whenever writing automation scripts you often need to zip some files, either to upload them or just for back-up purposes.

I wanted a really simple and easy way I can do this repeatedly so I thought of creating a PowerShell script that I can reuse to do this easily. I thought that PowerShell's pipeline would be a perfect tool for this. So I wanted to use the dir command with all its powerful filters etc and then just pipe this out to a zip file something like this:

PowerShell Zip Usage
1
2
3
4
5
6
Push-Location c:\temp
# create new zip file
dir -Recurse c:\temp\stufftoZip1 | ToZip -relativeBaseDirectory (Get-Location).Path  -fileName "c:\temp\test.zip"
# append to zip file
dir -Recurse c:\temp\stufftoZip2 | ToZip -relativeBaseDirectory (Get-Location).Path -appendToZip -fileName "c:\temp\test.zip"
Pop-Location

And that is all you need to make use of it. You can use all the query operators like select, where, select, first, etc.

This also introduces the concept of Pipeline functions.

Pipeline

Each pipeline function has a begin, process and end. In the begin block is where we initialize our function. In the process block this is where you handle each item from the pipeline. The end block is where we clean-up like close files etc.

PowerShell Pipeline function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ToZip($fileName, $relativeBaseDirectory=$null, [switch] $appendToZip=$false, $verbose=$true)
{
    begin
    {      
        # initialization
        $zipFile = [System.IO.Packaging.Package]::Open($fileName, $mode)
    }
    process
    {      
        # $_.FullName is the current item (in this case file) in the pipeline.
    }
    end
    {      
        # finalization
        $zipFile.Close();
    }
}

Implementation

In order to use the System.IO.Packaging.Package namespace and create Zip files you have to import the assembly WindowsBase as follows:

1
[Reflection.Assembly]::LoadWithPartialName("WindowsBase") | Out-Null

Save the following to a file and call it Zip.psm1

Zip.psm1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Set-StrictMode -V 1.0
[Reflection.Assembly]::LoadWithPartialName("WindowsBase") | Out-Null
function ToZip($fileName, $relativeBaseDirectory=$null, [switch] $appendToZip=$false, $verbose=$true)
{
    begin
    {
        $zipCreated = { (Get-Variable -ErrorAction SilentlyContinue -Name zipFile) -ne $null }
        $logMessage = {
            param ($message)
            if ($verbose)
            {
                Write-Host  $message
            }
        }
        $mode = [System.IO.FileMode]::Create
        if ($appendToZip)
        {
            $mode = [System.IO.FileMode]::Open
        }
        $zipFile = [System.IO.Packaging.Package]::Open($fileName, $mode)
    }
    process
    {
        if  ((&$zipCreated) -and ([System.IO.File]::Exists($_.FullName) -eq $true))
        {
             
            $zipFileName = $_.FullName
            if ($relativeBaseDirectory -ne $null)          
            {
                #$directoryName = [System.IO.Path]::GetDirectoryName($_.FullName)
                $zipFileName = $_.FullName.SubString($relativeBaseDirectory.Length, $_.FullName.Length-$relativeBaseDirectory.Length)              
            }
             
             
            $destFilename = [System.IO.Path]::Combine(".\\", $zipFileName)
            #$destFilename = $destFilename.Replace(" ", "_")
            $uri = New-Object Uri -ArgumentList ($destFilename, [UriKind]::Relative)
            $uri = [System.IO.Packaging.PackUriHelper]::CreatePartUri($uri)
             
            &$logMessage ("Adding: {0}" -f $destFileName)
             
            if ($zipFile.PartExists($uri))
            {
                $zipFile.DeletePart($uri);
            }
             
            $part = $zipFile.CreatePart($uri, [string]::Empty, [System.IO.Packaging.CompressionOption]::Normal)
            $dest = $part.GetStream()
             
            $srcStream = New-Object System.IO.FileStream -ArgumentList ($_.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
            try
            {
                $srcStream.CopyTo($dest)
            }
            finally
            {
                $srcStream.Close()
            }          
        }
    }
    end
    {
        if  (&$zipCreated)
        {
            $zipFile.Close()
        }
        &$logMessage "Done"
    }
}

Import & Usage

You can easily reuse the above module anywhere in your PowerShell scripts by importing it as follows:

PowerShell Zip Usage
1
2
3
4
5
6
7
8
9
10
Import-Module c:\Development\PowerShell\Zip.psm1
 
# Example 1
dir | ToZip c:\temp\test.zip
 
# Example 2
dir | ToZip -fileName c:\temp\test.zip -relativeBaseDirectory (Get-Location).Path
 
# Example 3
dir | ToZip -fileName c:\temp\test.zip -relativeBaseDirectory (Get-Location).Path -appendToZip

Example 1

Zip all the content in the current directory to c:\temp\test.zip

Example 2

Zip all the content in the current directory to c:\temp\test.zip. Using the folder structure starting at the current directory as relative path.

Example 3

Zip all the content in the current directory to c:\temp\test.zip. Using the folder structure starting at the current directory as relative path, and appending to the Zip.

Caveats

Actually there are none from PowerShell side, however the build in .net packaging library places a [Content_Types].xml in the zip file. You can't get rid of this. Also files with spaces will be escaped. This obviously can affect anything you zip and unzip where the file names are required. But again this is not PowerShell but the packaging library.

I will try and make a future post on a version that uses SharpZipLib so stay tuned...

Tuesday, 3 July 2012

PowerShell–Object Orientation

PowerShell does not have a class construct as a first class citizen. You can create objects either from .NET or creating CmdLets but how do you create an object from a pure PowerShell script?.

I came up with a solution previously I would like to share. However doing a bit of searching afterwards I found this project on CodePlex The solution looks pretty awesome so it is probably worth checking out as well.

The solution I came up with previously was to use PSObject and using Add-Member Cmdlet.

I wanted to be able to describe the function that I will be using as follows:

PowerShell Object Builder Definition
1
2
3
function New-ObjectBuilder()
{
}

You would use this as follows:

PowerShell Object Builder Usage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function New-MyObject()
{   
    $obj = New-ObjectBuilder
    $obj.AddBackingField("_Test", "testing")
    $obj.AddProperty("Test", [ref]$obj._Test)
    $obj.AddMethod("TestRun", {
        param ($aaa, $bbb)
        Write-Host "TestRun: " $this.Test, $aaa, $bbb
  
    });
    return $obj
}
$obj = New-MyObject
$obj.TestRun("a1", "b11")

And there is not really that much code to implement this function:

PowerShell Object Builder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function New-ObjectBuilder()
{
    $instance = new-object PSObject
     
    $instance | add-member -MemberType ScriptMethod -Name AddProperty -Value {
        param ($name, [ref]$backingField)
        $this | add-member -MemberType ScriptProperty -Name $name -Value {
            return $backingField.Value
        }.GetNewClosure() -SecondValue {
            $backingField.Value = $args[0]
        }.GetNewClosure()  
    }
     
    $instance | add-member -MemberType ScriptMethod -Name AddBackingField -Value {
        param ($name, $value = '')
        $this = Add-Member -InputObject $this -MemberType NoteProperty -Name $name -Value $value -PassThru
    }
 
    $instance | add-member -MemberType ScriptMethod -Name AddMethod -Value {
        param ($name, $codeBlock)
        $this | add-member -MemberType ScriptMethod -Name $name -Value $codeBlock
    }
     
    return $instance
}

You can now reuse this function and create new functions to construct your object