PowerShell Cookbook

Search

Categories

 

On this page

Why So Secure?
Get-Help –Online
Things more likely to kill you than Swine Flu
PowerShell Audio Sequencer
More Tied Variables in PowerShell
Want to Influence the PowerShell Cookbook V2?
Moving and Deleting Really Locked Files in PowerShell
Making Perfect Change with the Fewest Coins
More PowerShell Syntax Highlighting
PowerShell Script Encrypter

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 256
This Year: 7
This Month: 1
This Week: 0
Comments: 785

Sign In

 Monday, July 06, 2009
Monday, July 06, 2009 6:28:12 PM (Pacific Daylight Time, UTC-07:00) ( )

After working with PowerShell for awhile, then starting to write scripts, you quickly learn about PowerShell’s Execution Policy and other security features. You might even throw up your hands, and ask, “Why is this configuration so HARD? Why can’t it be as easy as VBS and Perl?” You can double-click on a VBS or CMD script and have it run automatically. Those scripts could invoke PowerShell anyways, so why put all this effort into security?

Double-click-to-run illustrates a weakness in vbscript’s design, not PowerShell’s. You could also double-click an EXE and have it do whatever you want. We’re concerned about being the entry point for an attack, not the means by which it is carried out.

The problem is that the world is extremely sensitive to scripting security. More sensitive than they are about EXE security. This is mostly driven by an over-reaction to a rude awakening: in the times of Melissa / ILoveYou, many people didn’t even know that these things called “scripts” were functionally equivalent to executables. That burned the Outlook developers, Exchange developers, network administrators, system administrators, and poor CEOs that thought a love letter must most naturally be about them.

During those outbreaks, millions of people watched their Outlook inbox fill with “love letters” from intelligent co-workers. Since the virus wasn’t a worm, each one of those was the result of somebody double-clicking on a script out of curiosity.

Fast forward a few years, and many of you already live in a world of retribution and over-reaction where “security experts” give guidance on how to remove WSH and scripting support from Windows: http://www.bing.com/search?q=uninstall+wsh. On the PowerShell team, we constantly hear from people impacted by these Mordac-like policies applied to PowerShell. Execution Policies often get abused as user restrictions, as misguided and ineffective as that is. See our Security Guiding Principles for more information about why Execution Policies are not user restrictions!

Being beyond reproach when it comes to security is crucial to PowerShell’s success. It’s the reason you can sing loudly from the rooftops for things such as this: http://it.slashdot.org/it/05/08/05/0411254.shtml.

Now, the lack of shell association indeed made it difficult to use PowerShell scripts from other automation programs in V1. While usability and security tend to conflict, we’re always looking for ways to get both. For example, in V2, we added the right-click option “Run with PowerShell” to address the interactive case. We added a “-File” parameter to PowerShell.exe to address the "dumb quoting and path translation" issues that used to make it difficult to launch PowerShell scripts from outside of PowerShell. We added an “–ExecutionPolicy” parameter (and other execution policy scopes) to let you use PowerShell.exe to automate tasks with no permanent system impact.

By writing a PowerShell script, you’re already among the very small population of computer users that understand scripting, security, and the impact of malware. It’s easy to lose track of the fact that not everybody has the same experience, background, and knowledge. PowerShell’s security features are designed to level the playing field for everybody else.

Comments [0] | | # 
 Tuesday, June 09, 2009
Tuesday, June 09, 2009 6:42:33 PM (Pacific Daylight Time, UTC-07:00) ( )

In CTP2 of PowerShell v2, we added a new parameter to Get-Help: Online. This parameter lets help authors declare an “online version” of their help topic, which PowerShell then launches with your default browser. For the PowerShell cmdlet help topics, we redirect to the excellent (and more frequently updated) online cmdlet help topics: http://technet.microsoft.com/en-us/library/dd347701.aspx.

PowerShell supports online help content for both MAML-based help topics (http://msdn.microsoft.com/en-us/library/bb525433(VS.85).aspx, http://blogs.msdn.com/powershell/archive/2006/09/14/Draft-Creating-Cmdlet-Help.aspx, http://blogs.msdn.com/powershell/archive/2008/12/24/powershell-v2-external-maml-help.aspx) and inline help comments (http://technet.microsoft.com/en-us/library/dd819489.aspx.)

To add support for the –Online parameter to your own cmdlet help, add a maml:navigationLink element with a maml:uri node to the maml:relatedLinks section in your MAML help. The help system uses the first navigation link that contains a URI as the target for the link.

<maml:relatedLinks>
              <maml:navigationLink>
                     <maml:linkText>Online version:</maml:linkText>
                     <maml:uri>http://go.microsoft.com/fwlink/?LinkID=113279</maml:uri>
              </maml:navigationLink>
(…)
</maml:relatedLinks>

When writing comment-based help, use the .LINK tag for the same effect:

001
002
003
004
005
006
007
008
## .SYNOPSIS
## Gets an object from the system
## .LINK
## http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx
function Get-MyObject
{
    "Hello!"
}

For more information about this feature, see Get-Help Get-Help –Parameter Online.

Comments [0] | | # 
 Wednesday, April 29, 2009
Wednesday, April 29, 2009 5:35:41 PM (Pacific Daylight Time, UTC-07:00) ( )

There are a few. Based on worldwide numbers (160 confirmed deaths, 3000 “suspected” cases)

  • Falling out of bed (900 confirmed deaths)
  • Falling down the stairs (1,690 confirmed deaths)
  • Big storm (874 confirmed deaths)
  • Drinking binge (346 confirmed deaths)

Of course, the statistic left out by the news

  • Normal flu (36,000 confirmed deaths per year)

http://www.nsc.org/research/odds.aspx

Comments [2] | | # 
 Monday, April 20, 2009
Monday, April 20, 2009 9:53:43 PM (Pacific Daylight Time, UTC-07:00) ( )

I got forwarded an addictive interactive sequencer yesterday (http://lab.andre-michelle.com/tonematrix) and was immediately hooked. I asked an internal mailing list if there was any kind of hardware that lets you do this kind of thing on the couch, and got the response -- “you mean MIDI?” That’s close, but it is closer to a very simplified sequencer.

I play classical guitar... even being a fan of electronic music, I had never seen a sequencer used, or tried to make anything in one. I’m sure some researcher out there would love to have me for a “out of touch with reality” anthropology study.

Then I wondered, “Why should GUI folks have all the fun?”

88 lines later, a PowerShell Sequencer / Tracker was born: http://www.leeholmes.com/projects/PsTracker/PsTracker.zip. Even as a jaded scripter, I’m constantly amazed how compact PowerShell is. Given an example input:

# Replace any dash with something else to make a sound in that spot.
# Format: <NOTE><OCTAVE> <PATTERN>
# If you restrict yourself to a pentatonic scale (i.e. CDEGAC), anything sounds good.
# Instruments: # ([Toub.Sound.Midi.GeneralMidiInstruments] | gm -static -mem Property | % { $_.Name } ) -join " "

# .Instrument OverdrivenGuitar

C5 ---------OO-OO--
A5 -------OO-OO---O
G4 --------------O-
E4 ----------------
D4 X---X---X---X---
C4 ----------------
A4 ----------------
G3 ----X-----------
E3 ----------------
D3 ----------------
C3 ----------------
A3 -------OO-OO----
G2 ----------------
E2 ----------------
D2 ----------------
C2 ----------------

# .Instrument SquareLead
C6 -X-X-XX-X--XXX

# .Instrument Ocarina
C7 ---------------X-X-XX-X--XXX

# .Instrument Kalimba
C8 -----------------X---X---X---X--

This is all it takes to process it:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
#requires -Version 2
param($path, $bpm)

$scriptPath = & { Split-Path $myInvocation.ScriptName }

$trackEntries = @{}

function Update-Track
{
    $trackEntries.Clear()
    $instrument = $null

    foreach($line in Get-Content $path)
    {
        if($line -match ".*Instrument (.+)([\s]*)$")
        {
            $instrument = $matches[1]
            if(-not $trackEntries[$instrument]) { $trackEntries[$instrument] = @{} }
        }
        elseif($line -notmatch "#|(^[\s]*$)")
        {
            $note,$measures = -split $line
            for($measure = 0; $measure -lt $measures.Length; $measure++)
            {
                if($measures[$measure] -ne "-")
                {
                    $trackEntries[$instrument][$measure] = @($trackEntries[$instrument][$measure] + $note)
                }
                $trackEntries[$instrument]["Length"] = [Math]::Max($trackEntries[$instrument]["Length"], $measure)
            }
        }
    }
}


$fsw = New-Object System.IO.FileSystemWatcher (Split-Path (Resolve-Path $path).ProviderPath),$path
Register-ObjectEvent $fsw Changed -SourceIdentifier TrackUpdated

Update-Track

Add-Type -Path (Join-Path $scriptPath "Toub.Sound.Midi.dll")
[Toub.Sound.Midi.MidiPlayer]::OpenMidi()

try
{
    $sleep = 250
    if($bpm) { $sleep = 1000 * 120 / (8 * $bpm) }

    $currentMeasures = @{}
    while($true)
    {
        $activeNotes = @()
   
        foreach($instrument in $trackEntries.Keys)
        {
            if(-not $currentMeasures[$instrument]) { $currentMeasures[$instrument] = 0 }
            $mappedInstrument = [Toub.Sound.Midi.GeneralMidiInstruments]::$instrument

            [Toub.Sound.Midi.MidiPlayer]::Play(
                (New-Object Toub.Sound.Midi.ProgramChange 0,0,$mappedInstrument) )
   
            foreach($note in $trackEntries[$instrument][$currentMeasures[$instrument]])
            {
                [Toub.Sound.Midi.MidiPlayer]::Play( (New-Object Toub.Sound.Midi.NoteOn 0,0,$note,127) )
                $activeNotes += New-Object Toub.Sound.Midi.NoteOff 0,0,$note,127
            }

            $currentMeasures[$instrument] =
                ($currentMeasures[$instrument] + 1) % (1 + $trackEntries[$instrument]["Length"])
               
        }

        Start-Sleep -m $sleep
        $activeNotes | % { [Toub.Sound.Midi.MidiPlayer]::Play($_) }

        if(Get-Event *TrackUpdated*)
        {
            Remove-Event TrackUpdated
            Update-Track
        }
    }
}
finally
{
    [Toub.Sound.Midi.MidiPlayer]::CloseMidi()
    Unregister-Event TrackUpdated
    Remove-Event *TrackUpdated*
}

 

For example:

.\Start-Tracker track.txt 60

If your system has a MIDI instrument for “Cowbells,” make sure to add more of them! This script builds on Stephen Toub's MIDI library, which I can't seem to find a reference to any longer.

As an aside, that research junket eventually led me to playing with a more feature-rich (free) sequencer called Linux Multimedia Studio. Keeping with the basis of starting with a pentatonic scale, this took only about an hour or two: http://www.leeholmes.com/projects/PsTracker/strive.mp3.

 

Comments [0] | | # 
 Thursday, March 26, 2009
Thursday, March 26, 2009 10:51:52 PM (Pacific Daylight Time, UTC-07:00) ( )

Ibrahim just posted something to the PowerShell blog about how to create tied variables in PowerShell. If you extend this approach with script blocks, you have a very powerful dynamic scripting technique.

PS C:\temp> cd \temp
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:lee { $myTestVariable } { $GLOBAL:myTestVariable = 2 * $args[0] }
PS C:\temp> $lee
PS C:\temp> $lee = 10
PS C:\temp> $lee
20
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:today { (Get-Date).DayOfWeek }
PS C:\temp> $today
Wednesday
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:random -Get { Get-Random } -Set { Get-Random -SetSeed $args[0] }
PS C:\temp> $random
1740776676
PS C:\temp> $random
1507521897
PS C:\temp> $random = 10
PS C:\temp> $random
1613858733
PS C:\temp> $random = 10
PS C:\temp> $random
1613858733

He alluded to it in the post – here is the full text of the script:

(Edit 05/17: Updated to make the getters more like PowerShell pipelines: return a single object, or collection of PSObject)

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
## New-ScriptVariable.ps1
param($name, [ScriptBlock] $getter, [ScriptBlock] $setter)

Add-Type @"
using System;
using System.Collections.ObjectModel;
using System.Management.Automation;

namespace Lee.Holmes
{
    public class PSScriptVariable : PSVariable
    {
        public PSScriptVariable(string name,
            ScriptBlock scriptGetter, ScriptBlock scriptSetter)
            : base(name, null, ScopedItemOptions.AllScope)
        {
            getter = scriptGetter;
            setter = scriptSetter;
        }
        private ScriptBlock getter;
        private ScriptBlock setter;

        public override object Value
        {
            get
            {
                if(getter != null)
                {
                    Collection<PSObject> results = getter.Invoke();
                    if(results.Count == 1)
                    {
                        return results[0];
                    }
                    else
                    {
                        PSObject[] returnResults = new PSObject[results.Count];
                        results.CopyTo(returnResults, 0);
                        return returnResults;
                    }
                }
                else { return null; }
            }
            set
            {
                if(setter != null) { setter.Invoke(value); }
            }
        }
    }
}
"@


if(Test-Path variable:\$name)
{
    Remove-Item variable:\$name -Force
}
$executioncontext.SessionState.PSVariable.Set(
    (New-Object Lee.Holmes.PSScriptVariable $name,$getter,$setter))

 

Comments [5] | | # 
 Wednesday, March 11, 2009
Thursday, March 12, 2009 5:41:25 AM (Pacific Daylight Time, UTC-07:00) ( )

We've started working on the next edition of the PowerShell Cookbook, and one obvious goal is to improve on the first version.

As the first version has been in print, I've taken notes on where people get confused with certain recipes. I've taken notes on what I felt were content gaps, and taken the feedback from reviews on Amazon.com and random blogs. Reviews on Amazon are GOLD for authors. They help readers form educated opinions, and provide helpful feedback about the book itself. If you want to thank the author of a book you like, write a review on Amazon.

The second edition of the PowerShell Cookbook continues in the same tradition as the first. Topical, real-world solutions to everyday problems. Packed with an appendix of reference material that matters. It will continue to be a purposefully distinct approach from PowerShell in Action.

With that, here's your chance to influence the next edition. What did you find too basic? Too advanced? Missing altogether? Were there any recurring issues with the approach or content?

Another question we're pondering is the unique value that the printed edition brings to the table. Much of the content in the PowerShell Cookbook was pre-published to this blog, newsgroups, or other channels. Many of the topics it addresses can be found through internet searches and forums. Many copies are floating around on Bit Torrent. Given all of that, why did you still purchase the printed version?

I know -- a lot of questions, very few answers! Let 'er rip.

Comments [6] | | # 
 Tuesday, February 17, 2009
Tuesday, February 17, 2009 11:55:19 PM (Pacific Standard Time, UTC-08:00) ( )

Once in awhile, you need to do brain surgery on files locked by the system. This is a common problem run into by patches and hotfixes, so Windows has a special mechanism that lets it move files before any process has the chance to get its grubby little hands on it. This can only be done during a reboot, leading to the dire warning given to you by many installers.

The Win32 API that enables this is MoveFileEx. Calling this API with the MOVEFILE_DELAY_UNTIL_REBOOT flag tells Windows to move (or delete) your file at the next boot.

Here’s how to do it from PowerShell:

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024

function Move-LockedFile
{
    param($path, $destination)

    $path = (Resolve-Path $path).Path
    $destination = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($destination)

    $MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004

    $memberDefinition = @'
    [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
    public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName,
       int dwFlags);
'@


    $type = Add-Type -Name MoveFileUtils -MemberDefinition $memberDefinition -PassThru
    $type::MoveFileEx($path, $destination, $MOVEFILE_DELAY_UNTIL_REBOOT)
}

[C:\Windows\system32\config\txr]
PS:181 > dir -force | % { Move-LockedFile $_.Name (Join-Path c:\temp\txr ($_.Name + ".Bak")) }

[C:\Users\leeholm]
PS:182 > dir -Filter "NTUser.DAT{*" -force | % { Move-LockedFile $_.Name (Join-Path c:\temp\txr ($_.Name + ".Bak")) }

Comments [2] | | # 
 Monday, February 09, 2009
Tuesday, February 10, 2009 5:16:40 AM (Pacific Standard Time, UTC-08:00) ( )

I've long wondered exactly how few coins you need in your pocket in order to perfectly round out any bill. I usually grab a handful and hope it works out. Even that mathematically astute technique sometimes leaves me a nickel or few pennies short, though, so I settle for making change that gets me a quarter back instead of yet another handful of ore.

Even this settle-for-second-best option isn't that great. It can cause permanent damage to unsuspecting cashiers that aren't so good at math. Wondering why you would ever give them $1.13 for a $0.88 bill, they'll often just give you your change back AND then the stack of coins they were originally planning to load you up with.

Well, no longer.

It turns out that you need exactly 10 coins in your pocket: 3 quarters, 2 dimes, 1 nickel, and 4 pennies. With these in your arsenal, you can make perfect change for any bill.

How can you be sure? Brute force is your friend, as is PowerShell, of course.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
$coins = @{ 0.25 = 0; 0.10 = 0; 0.05 = 0; 0.01 = 0 }

function SelectCoins([Decimal] $change)
{
    $result = $coins.Clone()

    foreach($denomination in $coins.Keys | Sort -Desc)
    {
        while($change -ge $denomination)
        {
            $change -= $denomination
            $result[$denomination]++
        }
    }
   
    $result
}

$results = 1..99 | % { SelectCoins ($_ / 100) }

foreach($denomination in $coins.Keys | Sort -Desc)
{
    ("{0:c}: " -f $denomination) +
        ($results | % { $_[$denomination] } | 
            Measure-Object -Max).Maximum
}

 

Gives:

$0.25: 3
$0.10: 2
$0.05: 1
$0.01: 4

Comments [1] | | # 
 Tuesday, February 03, 2009
Tuesday, February 03, 2009 6:40:56 PM (Pacific Standard Time, UTC-08:00) ( )

Vladimir Averkin recently wrote a series of posts that show how to export code with syntax highlighting into HTML and RTF formats. It works great in Outlook, but was causing Windows Live Writer to crash. The reason is that the HTML stream of the clipboard isn’t just a blob of HTML – it’s supposed to be placed into the clipboard as CF_HTML. Investigation of that issue gave enough information to exactly pinpoint the crash in Live Writer, which they were quick to resolve once we pointed out. So it was a positive thing after all :)

While fixing the script, I took the opportunity to make the HTML prettier, work from both the ISE and the command-line, and fix a few bugs. I’ve posted it here: http://www.leeholmes.com/projects/scripts/Set-ClipboardScript.ps1.txt, as well as below.

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
################################################################################
# Set-ClipboardScript.ps1
#
# The script entire contents of the currently selected editor window to system
# clipboard. The copied data can be pasted into any application that supports
# pasting in UnicodeText, RTF or HTML format. Text pasted in RTF or HTML
# format will be colorized.
#
# See also:
# http://blogs.msdn.com/powershell/archive/2009/01/13/
# how-to-copy-colorized-script-from-powershell-ise.aspx
# http://www.leeholmes.com/blog/SyntaxHighlightingInPowerShell.aspx
# http://www.leeholmes.com/blog/RealtimeSyntaxHighlightingInYourPowerShellConsole.aspx
#
################################################################################

[CmdletBinding()]
param($path)

function Get-ScriptName
{
    $myInvocation.ScriptName
}

if($path -and ([Threading.Thread]::CurrentThread.ApartmentState -ne "STA"))
{
    PowerShell -NoProfile -STA -File (Get-ScriptName) $path
    return
}

$tokenColours = @{
    'Attribute' = '#FFADD8E6'
    'Command' = '#FF0000FF'
    'CommandArgument' = '#FF8A2BE2'
    'CommandParameter' = '#FF000080'
    'Comment' = '#FF006400'
    'GroupEnd' = '#FF000000'
    'GroupStart' = '#FF000000'
    'Keyword' = '#FF00008B'
    'LineContinuation' = '#FF000000'
    'LoopLabel' = '#FF00008B'
    'Member' = '#FF000000'
    'NewLine' = '#FF000000'
    'Number' = '#FF800080'
    'Operator' = '#FFA9A9A9'
    'Position' = '#FF000000'
    'StatementSeparator' = '#FF000000'
    'String' = '#FF8B0000'
    'Type' = '#FF008080'
    'Unknown' = '#FF000000'
    'Variable' = '#FFFF4500'
}

if($psise)
{
    $tokenColours = $psise.Options.TokenColors
}

Add-Type -Assembly System.Web
Add-Type -Assembly PresentationCore

# Create RTF block from text using named console colors.
function Append-RtfBlock ($block, $tokenColor)
{
    $colorIndex = $rtfColorMap.$tokenColor
    $block = $block.Replace('\','\\').Replace("`r`n","\cf1\par`r`n")
    $block = $block.Replace("`t",'\tab').Replace('{','\{').Replace('}','\}')
    $null = $rtfBuilder.Append("\cf$colorIndex $block")
}

# Generate an HTML span and append it to HTML string builder
$currentLine = 1
function Append-HtmlSpan ($block, $tokenColor)
{
    if (($tokenColor -eq 'NewLine') -or ($tokenColor -eq 'LineContinuation'))
    {
        if($tokenColor -eq 'LineContinuation')
        {
            $null = $codeBuilder.Append('`')
        }
       
        $null = $codeBuilder.Append("<br />`r`n")
        $null = $lineBuilder.Append("{0:000}<BR />" -f $currentLine)
        $SCRIPT:currentLine++
    }
    else
    {
        $block = [System.Web.HttpUtility]::HtmlEncode($block)
        if (-not $block.Trim())
        {
            $block = $block.Replace(' ', '&nbsp;')
        }

        $htmlColor = $tokenColours[$tokenColor].ToString().Replace('#FF', '#')

        if($tokenColor -eq 'String')
        {
            $lines = $block -split "`r`n"
            $block = ""

            $multipleLines = $false
            foreach($line in $lines)
            {
                if($multipleLines)
                {
                    $block += "<BR />`r`n"
                   
                    $null = $lineBuilder.Append("{0:000}<BR />" -f $currentLine)
                    $SCRIPT:currentLine++
                }

                $newText = $line.TrimStart()
                $newText = "&nbsp;" * ($line.Length - $newText.Length) + 
                    $newText
                $block += $newText
                $multipleLines = $true
            }
        }
   
        $null = $codeBuilder.Append(
            "<span style='color:$htmlColor'>$block</span>")
    }
}

function GetHtmlClipboardFormat($html)
{
    $header = @"
Version:1.0
StartHTML:0000000000
EndHTML:0000000000
StartFragment:0000000000
EndFragment:0000000000
StartSelection:0000000000
EndSelection:0000000000
SourceURL:file:///about:blank
<!DOCTYPE HTML PUBLIC `"-//W3C//DTD HTML 4.0 Transitional//EN`">
<HTML>
<HEAD>
<TITLE>HTML Clipboard</TITLE>
</HEAD>
<BODY>
<!--StartFragment-->
<DIV style='font-family:Consolas,Lucida Console; font-size:10pt;
    width:750; border:1px solid black; overflow:auto; padding:5px'>
<TABLE BORDER='0' cellpadding='5' cellspacing='0'>
<TBODY>
<TR>
    <TD VALIGN='Top'>
<DIV style='font-family:Consolas,Lucida Console; font-size:10pt;
    padding:5px; background:#cecece'>
__LINES__
</DIV>
    </TD>
    <TD VALIGN='Top' NOWRAP='NOWRAP'>
<DIV style='font-family:Consolas,Lucida Console; font-size:10pt;
    padding:5px; background:#fcfcfc'>
__HTML__
</DIV>
    </TD>
</TR>
</TBODY>
</TABLE>
</DIV>
<!--EndFragment-->
</BODY>
</HTML>
"@


    $header = $header.Replace("__LINES__", $lineBuilder.ToString())
    $startFragment = $header.IndexOf("<!--StartFragment-->") +
        "<!--StartFragment-->".Length + 2
    $endFragment = $header.IndexOf("<!--EndFragment-->") +
        $html.Length - "__HTML__".Length
    $startHtml = $header.IndexOf("<!DOCTYPE")
    $endHtml = $header.Length + $html.Length - "__HTML__".Length
    $header = $header -replace "StartHTML:0000000000",
        ("StartHTML:{0:0000000000}" -f $startHtml)
    $header = $header -replace "EndHTML:0000000000",
        ("EndHTML:{0:0000000000}" -f $endHtml)
    $header = $header -replace "StartFragment:0000000000",
        ("StartFragment:{0:0000000000}" -f $startFragment)
    $header = $header -replace "EndFragment:0000000000",
        ("EndFragment:{0:0000000000}" -f $endFragment)
    $header = $header -replace "StartSelection:0000000000",
        ("StartSelection:{0:0000000000}" -f $startFragment)
    $header = $header -replace "EndSelection:0000000000",
        ("EndSelection:{0:0000000000}" -f $endFragment)
    $header = $header.Replace("__HTML__", $html)
   
    Write-Verbose $header
    $header
}

function Main
{
    $text = $null
   
    if($path)
    {
        $text = (Get-Content $path) -join "`r`n"
    }
    else
    {
        if (-not $psise.CurrentOpenedFile)
        {
            Write-Error 'No script is available for copying.'
            return
        }
       
        $text = $psise.CurrentOpenedFile.Editor.Text
    }

    trap { break }

    # Do syntax parsing.
    $errors = $null
    $tokens = [system.management.automation.psparser]::Tokenize($Text,
        [ref] $errors)

    # Initialize HTML builder.
    $codeBuilder = new-object system.text.stringbuilder
    $lineBuilder = new-object system.text.stringbuilder
    $null = $lineBuilder.Append("{0:000}<BR />" -f $currentLine)
    $SCRIPT:currentLine++
  

    # Initialize RTF builder.
    $rtfBuilder = new-object system.text.stringbuilder
   
    # Append RTF header
    $header = "{\rtf1\fbidis\ansi\ansicpg1252\deff0\deflang1033{\fonttbl" +
        "{\f0\fnil\fcharset0 $fontName;}}"
    $null = $rtfBuilder.Append($header)
    $null = $rtfBuilder.Append("`r`n")

    # Append RTF color table which will contain all Powershell console colors.
    $null = $rtfBuilder.Append("{\colortbl ;")
   
    # Generate RTF color definitions for each token type.
    $rtfColorIndex = 1
    $rtfColors = @{}
    $rtfColorMap = @{}
   
    [Enum]::GetNames([System.Management.Automation.PSTokenType]) | % {
        $tokenColor = $tokenColours[$_];
        $rtfColor = "\red$($tokenColor.R)\green$($tokenColor.G)\blue" +
            "$($tokenColor.B);"
        if ($rtfColors.Keys -notcontains $rtfColor)
        {
            $rtfColors.$rtfColor = $rtfColorIndex
            $null = $rtfBuilder.Append($rtfColor)
            $rtfColorMap.$_ = $rtfColorIndex
            $rtfColorIndex ++
        }
        else
        {
            $rtfColorMap.$_ = $rtfColors.$rtfColor
        }
    }
   
    $null = $rtfBuilder.Append('}')
    $null = $rtfBuilder.Append("`r`n")
   
    # Append RTF document settings.
    $null = $rtfBuilder.Append('\viewkind4\uc1\pard\f0\fs20 ')
   
    # Iterate over the tokens and set the colors appropriately.
    $position = 0
    foreach ($token in $tokens)
    {
        if ($position -lt $token.Start)
        {
            $block = $text.Substring($position, ($token.Start - $position))
            $tokenColor = 'Unknown'
            Append-RtfBlock $block $tokenColor
            Append-HtmlSpan $block $tokenColor
        }
       
        $block = $text.Substring($token.Start, $token.Length)
        $tokenColor = $token.Type.ToString()
        Append-RtfBlock $block $tokenColor
        Append-HtmlSpan $block $tokenColor
       
        $position = $token.Start + $token.Length
    }

    # Append RTF ending brace.
    $null = $rtfBuilder.Append('}')
   
    # Copy console screen buffer contents to clipboard in three formats -
    # text, HTML and RTF.
    $dataObject = New-Object Windows.DataObject
    $dataObject.SetText([string]$text, [Windows.TextDataFormat]"UnicodeText")
    $rtf = $rtfBuilder.ToString()
    $dataObject.SetText([string]$rtf, [Windows.TextDataFormat]"Rtf")
    $code = $codeBuilder.ToString()
    $html = GetHtmlClipboardFormat($code)
   
    $dataObject.SetText([string]$html, [Windows.TextDataFormat]"Html")

    [Windows.Clipboard]::SetDataObject($dataObject, $true)
}

. Main
Comments [6] | | # 
 Monday, February 02, 2009
Monday, February 02, 2009 5:05:46 PM (Pacific Standard Time, UTC-08:00) ( )

We frequently get questions asking, “Where can I get a PowerShell script encoder so I can write secure scripts like the Visual Basic Script Encoder?”

The answer is that it is impossible to hide the password from the user if the script ever needs it. This is true of PowerShell, VBScript, C#, C++, Assembly, or any other language. There will always be some point when your script has reversed all of the encryption / protection mechanisms, giving the “attacker” complete access to it. If you don’t want the password itself hanging around in a script file, you can prompt the user for it. If the user is never supposed to know it, then you need to re-think your architecture.

Microsoft hasn’t been clear enough documenting what protections the Script Encoder offers, but here is an excerpt from the Scripting Guys:

Now, the important thing to keep in mind is that the script is simply encoded (or obfuscated); it is definitely not encrypted. What does that mean? That means the encoder will hide your script from most people; however, a truly determined hacker - armed with a knowledge of codes or armed with a utility downloaded from the Internet - could crack the code. Among other things, that means that you should never do something like “hide” an Administrator password in a script and assume that the Script Encoder will keep it safe from prying eyes. It won’t. It’s an encoder, not an encrypter, and there’s definitely a difference.

I’m not sure why the main download page is fond of the term “determined hacker” – a 30 second search for “vbe decryption” returns pages of results.

Now, a valid response to the whole situation is that you really only want to deter casual investigation, or that reversing the protection can then be linked to a breach of contract or software license. If you are in either of those boats, you don’t need an official tool to do this for you. Hiding your script behind Base64 encoding or ROT-13 should offer plenty of protection, and takes only a few lines of scripting. If you have the skill to make that decision, you have the skill to implement it as well.

Comments [2] | | #