Monday, June 30, 2014

Using PowerShell to Manage - Part 2: Using PowerShell to register your Box app on your workstation

If you've gotten here before reading part 1 of this series you want to read that first.  I has some important information that will help make sense of what this post is about.

Now that we've registered our application with, we need to grant our Box account access to use it.  I created a function in PowerShell that will bring you to the Box app site (which allows you to grant permission) and then grabs the information returned by Box and write it to your workstation's registry.

Just a little back ground (for a complete understanding read Box's official OAuth 2.0 document).

Basic layman's terms
In order to use the app you registered with Box, Box's OAuth2 implementation requires that you grant your Box account access to the application.  The granting process is an interactive process within your browser.  So you can't just write a script to carry out the granting process.  Once you've granted permission to the app, Box will redirect your browser (remember the redirect_uri) to a site you configured in the app.  The address bar contains the URI specified by the redirect_uri and appends a security token (which will be arbitrarily set during the registration process) and the initial authorization code.

The initial authorization code has a very short life.  It will expire within 30 seconds.  So if you don't use it to to retrieve your access token in time, you will have to go through the granting process again.
NOTE:  You can go through the granting process as many times as you like.  My PowerShell function actually makes it a rather painless process.

Once you get your authorization code you will need to leverage Box's RESTful API (within 30 seconds) to obtain an access token.  The API returns 5 attributes that my PowerShell function writes to your workstation's registry.

  • access_token #This is the actual token needed to issue commands against Box's APIs
  • expires_in #This is the life of the token (always roughly 1 hour give or take a few minutes)
  • restricted_to #This always comes back blank for me and I haven't had a need for this attribute
  • refresh_token #After the one hour expiration of your access token you will need to use this token to get a new access token.  All of my functions first check to see if the current access token is expired, if so it will refresh automatically.
  • token_type #I'm not sure of all the types, but we will be dealing with the 'bearer' type.
So let's take a look at the actual PowerShell function that does all of that:

NOTE:  I make calls to Test-RegKey, Set-RegKey, Set-RegValue.  These are not cmdlets that are built into PowerShell.  Instead, they are part of a suite of registry function that I created during one of the Scripting Games I participated in a few years ago.  I keep them in a separate module which you can download here.

I re-wrote the function to use PowerShell's native ability to access the registry (new-item, set-itemproperty, get-itemproperty, test-path).  You no longer need to import the RegEditSuit.psm1 module.

Here is the function:

Let's break the function down:
First the parameters:

  • $AppName 
    • This is actually an arbitrary name you give for registration purposes.  I used the same name I provided when I registered my application on  While it arbitrary, remember that a registry key will be created for this app and that the Box function I create reference the registry key that gets created.  So you will want to remember the name you give it.
  • $ClientID
    • You recorded this string when you registered the application on  You will need to provide that string.
  • $ClientSecret
    • You recorded this string when you registered the application on  You will need to provide that string.
  • $APIKey
    • This is actually the $ClientID.  I personally like to record it into my workstation registry as APIKey, but it is not required to use any of the Box functions I created.
  • $SecurityKey
    • This is an arbitrary string.  The function has a default one.  The official Box documentation recommends that you use an anti-forgery state token to prevent CSRF attacks to your users.  But I believe that is for mobile apps you will publishing to their app store.  I don't think that forgery is an issues for colleagues that may be using your PowerShell scripts (imo).
So the first thing the script does it to make sure that a registry key exists under HKLM\software\\.  If it doesn't exist then it creates it.

You can register multiple apps.

It then records the Client_ID, Client_Secret, and the APIKey at the root of that registry key.
It next runs through the interactive granting authorization process to retrieve the authentication code.  It starts by opening IE and navigating to a URI that includes the Client_ID and the arbitrary security key.  Then pops up a msgbox with instructions to finish the interactive authorization in IE before clicking OK. 

Let's take a look at what that process like in IE.
IE Opens to an Authentication Page
Once Authorized Grant or Deny Access
The site I land on I created additional instructions.  The green box shows the arbitrary security key for validation and the red box shows the authorization code.
Back in PowerShell click the OK button and PowerShell will grab the authorization code out of the LocationURL value.

Back to 'Register-BoxApp' function:
Once you click OK the function next loads up a hashtable with the AuthCode, clientID, clientSecret, it sets the RESTful URI and then invokes the RestMethod.

Here is the outcome in PowerShell of waiting too long (more that 30 seconds) to click the OK button.
Here is the outcome in PowerShell if you successfully click the OK button with 30 seconds.
If you wait too long to click OK this is the error you get.  Code is good for 30 seconds.
Results for successfully clicking OK within 30 seconds.

You may notice that after the Invoke-RestMethod line in the Register-BoxApp function, if there is a Access Token, the script will run the 'Register-BoxAccessToken'.  Here is what that function looks like:
Note:  I re-wrote the function to use PowerShell's native ability to access the registry (new-item, set-itemproperty, get-itemproperty, test-path).

Because the return data type of Invoke-RestMethod to Box is JSON, Invoke-RestMethod returns a PSCustomObject.  That means I have to provide the property names of the object ($RegValueNames) - unlike a hashtable where I could request the keys and interate through the key names to accomplish the same thing the function does with the PSCustomObject.

You will notice in the 'Register-BoxAccessToken' function as I'm iterating through the valuenames I check to see if the value name equals 'expires_in'.  That is because the data returned from Box is the number of seconds the access code is good before it expires.  So I create a datetime object and add the seconds to it and convert it to a string to store in the registry.  In a later post you will see how a function called 'Get-BoxAccessToken' exploits that datetime object in order to determine if the token needs to be refreshed.

If all goes well then AccessTokenInfo is written to your workstation's registry.
BoxAccessToken is a sub key and the access token, expires_in, and refresh token are registered here.

In the next post, I will discuss the functions 'Get-BoxAccessToken' and 'Refresh-BoxAccessToken, which are incorporated in all the work functions you will create in the future.

I've uploaded the entire OAuth2 PowerShell module for you to grab here.

Friday, June 27, 2014

Using PowerShell to Manage - Part 1: Prepping for OAuth2 Protocol.

Wow!!! It's been quite a long time since my last post.  Life is so busy.

Anyway, I struggled for some time with RESTful APIs and finally got my head around it when I had to create some functions for my logical access team to manage on-boarding new enterprise users into (Box).  So I thought I'd share three modules I created around the process.

Even if you don't use the modules, they do demonstrate how easy it is to use the 'Invoke-RestMethod'.  It will also show you how to implement OAuth2 against Box's implementation of the protocol (which may be helpful).

As the title indicates this series of posts will be specific to Box's APIs.  It doesn't mean you can't use the same principles with other RESTful APIs or other OAuth2 sites, but my experience is specific to Box and so the information will be specific to Box.  I will discuss:

  • Registering an application with Box on their developer site.
  • Granting users access to your Box application.
  • Registering your Box app in your workstation registry.
  • Understanding and using the OAuth2 access token, refresh token, and token expiration.
  • How to exploit Box API and convert curl commands to PowerShell functions.
  • Create PowerShell functions using 'Invoke-RestMethod' for various tasks.
The 'Using PowerShell to Manage' series of posts are a compilation of information I successfully used in my environment.  I am in no way claiming that this is the recommended method or the only way of using PowerShell to manage Box.  This information is what I have actually successfully developed for my requirements.
Also I am making three modules available to download.  I use aliases; commenting and help are sparse or even non-existent in the modules; so if that bothers you, just don't use the modules instead of being critical about it.
The documentation around using PowerShell to exploit RESTful APIs on the Internet is sparse at best, and using PowerShell to manage Box almost non-existent.  So I'm providing this to hopefully help other PowerShellers if they need to implement automation, or functionality in a similar fashion around other RESTful APIs or even specifically around the Box APIs.

So let's get to it.

When you want to use PowerShell to manage Box you will need to register an application with  Imagine you want to create a mobile app to exploit (they have SDKs for node, python, ruby, php, and haskell), in this case we are using PowerShell and will be accessing our application via a PowerShell console instead of a mobile device.

Let's look at some screen shots of creating a Box application.  I will be showing screen shots via my personal account (not my enterprise account).  The enterprise interface with Box is only slightly different.  If you are doing this via an enterprise account you should still be able to follow the process.

On your first login or upon clicking the link 'Create a Box Application' the following page appears:
  • If you are in an enterprise environment the 'Box Content' API will allow you to manage user accounts, create folders, manage permissions on folders, create 'Shared Links', etc...  
  • The 'Box View' API, allows you to manage the actually data like file views, file conversions, etc...  My functions were built for my logical access team so will exclusive be using the 'Content' API.
You will want to give your application a name (I've called my 'PowerShell Demo') and choose the API you want the application to use and click 'Create Application'.

Now you will want to configure your application and record some important information that will be needed in order to allow other users (even yourself) to use your PowerShell application.

Fill in the General info as you wish, none of that section will actually have any affect on you PowerShell code.

The OAuth2 Parameters section is the most critical part of this step.

You will want to record your client_id and client_secret for future reference (just paste it Notepad or something like that).

The redirect_uri was the most confusing portion to me (probably because I'm just stupid - cause it made total sense once I figured it out).  Anyway, you'll want to set this to any web server that will handle SSL.  In my case my team uses a utility server that had IIS installed on it already.  I just grabbed a cert from my AD forest CA for the URL that I wanted to use and threw it on that server.

The redirect_uri is a URI that will redirect your browser.  Box doesn't actually write to this URI.  Box actually appends the redirect_uri with your initial authorization code and an arbitrary token.

This was probably the most confusing part of the OAuth2 process.  I was under the impression that Box needed access to the site to be able to place a file or some data that would be interpreted in your browser when redirected.  I searched for free public sites with this type of access in order to set my redirect_uri; and I read dozens and dozens of documents with no success of understanding this (I know, I know -- I'm stupid, right?).

So just to reiterate:  The redirect_uri is not a site that Box needs to write to.  It is a landing site for your browser and Box appends the URI in your address bar in your browser with the needed authorization code and the arbitrary token you provided on making the request for the authorization code.

The PowerShell script I'm going to show you next will automagically grab this authorization code and register your app in your workstation registry.  I'll show you that shortly.

Back to the config page on
If you scroll to the bottom of the configuration page you will see a section 'Backend Parameters'.  You will notice that the 'API Key' is the same as the client_id.  I make this distinction because if you want help from Box with your app they will want your API Key and that is it.  My PowerShell function records the API Key in your workstation's registry even though none of my other functions will ever actually use it.

When I first built the function I thought it might be import to distinguish between client_id and 'API Key'.  It ended up being a mute issue and isn't really a necessary part of the registration process in the PowerShell script I'm about to show you.  I'm keeping my function the way it is just in case I ever have a need for the distinction, but you can remove it from the register function if you so chose (don't worry you'll understand once we go over the function).

Save your application.

Okay, we've registered our application with  We are done prepping our Box application and can now use our OAuth2 protocol to get access tokens and use the API to automate Box functionality.

Part 2 of this series will show how to register the important client_id and client_secret on your workstation; how to retrieve your initial authorization code, access token, refresh token, and token expiration and write that information to your workstation's registry.  We will also see how to refresh your access token when it expires.  We will dive into how Box implemented the OAuth2 protocol and how PowerShell can exploit their implementation.

Thursday, February 25, 2010

The Power of -Split

I work with Active Directory everyday. Sometimes, when I'm exporting information, I want to be able to break down the distinguishedName property to just give me the actual name. For example, in our environment we create names in AD with LastName, FirstName. The distguishedName for a user name like that ends up looking like this:

cn=Doe\, Jane,ou=users,ou=location,dc=example,dc=com

If I want to get just the name without the escape character or all the extra path info that makes up a distinguished name without PowerShell I'm probably going to copy the whole name to an editor and manually remove everthing to leave just Doe, Jane (probably just easier to type Doe, Jane instead of all of that copying stuff). But what happens if I need to get just the LastName, FirstName of a thousand people. Now I've some problems. I will have to export the DN's from AD, copy them to Word or Excel and then do a bunch of replacements or string manipulation on them.

Or I can write a PowerShell script to give them to me:

function Get-NameFromDN([string]$DN){            
    return ($DN.replace('\','') -split ",*..=")[1]            
Get-QADObject | %{Get-NameFromDN $_.distinguishedName}            

Did you notice that I can split the string with a [regex] delimiter?  That is really cool.  Try the split yourself; paste the string below into a PowerShell prompt and hit enter:

"cn=mygroupobjectname,ou=security groups,ou=location,dc=example,dc=com" -split ",*..="

That produces:

PS C:>"cn=mygroupobjectname,ou=security groups,ou=location,dc=example,dc=com" -split ",*..="            
security groups            

The [regex] expression ",*..=" says find a pattern that matches zero or more commas any two characters and an equal sign.  That would match any of the following:

This pattern becomes my delimiter so that when the split function runs it will split the string where ever it finds the match.  It puts the remaining elements in a zero based array.  Because a DN starts with a pattern match the first element of the array [0] will be empty.  That is why in my Get-NameFromDN function above I retrieve element [1].

PowerShell has a nice help article that explains the full functionality of -split.  I recommend that you read it to see just how powerful -split is.  You can find the help article by issuing the command:

Get-Help about_split

I used this example to show the power of the split operation, but you might be wondering just how practical this is.  Well as it turns out I have a scheduled tasks that runs every week that does a csvde export of ActiveDirectory.  We keep the csv files around for several months just in case we delete a user and need to recreate it we have all the info we need for the recreation.

When you delete a user account all of the group memberships are deleted.  So even if you use tools like ADFind and ADMod or Hyena to restore the account in AD, you will still need to get a list of groups the account belonged to from somewhere.  If you haven't backed up AD you need to query the user and they probably wont know and the whole process becomes time consuming.  That is why I do a csvde export for every domain I manage.  The csvde export creates a column in the csv file called memberof and in that column concatenates the distinguishedNames of all the non-principal groups the account belongs to with a semi-colon.  You can't just copy and paste the groups from the export into ADUC to add the groups, because, as mentioned above, ADUC's add group dialog won't accept distinguished names.  You could create a script to use the DN which would be fine.  But what if, like me, you needed to just supply the groups to your Logical Access team and they don't have any scripting ability.  Well you can run a script like this:

function Get-NameFromDN([string]$DN){            
    return ($DN.replace('\','') -split ",*..=")[1]            
$lastname = ""            
<#The following produces a list of group DNs that are found in the csv for the users with the last name specified.  
To narrow your search you could -match on something else like 'lastname\, firstname' or a whole different column altogether.  
Import-Csv .\adexport.csv | ?{$_.DN -match 'lastname' -and $_.objectclass -eq 'user'} | %{$_.memberof.split(';')}            
<#However, you can't just copy the output, and send it to the LogicalAccess admin to paste in ADUC.  ADUC's add group 
dialog won't accept DN's.  So add the function at the end and now you get a list of names that you can copy and paste in 
an email to your LogicalAccess admin for him/her to create the account with.
Import-Csv .\adexport.csv | ?{$_.DN -match 'lastname' -and $_.objectclass -eq 'user'} | %{$_.memberof.split(';')} | %{Get-NameFromDN $_}             

The first Import-Csv produces a list of group DNs.  I can't use that for ADUC.

The second Import-Csv produces what I want.  Here is what it does.

The second Import-Csv line passes each line of the csv file to the pipeline where a match is performed in both the DN column and the objectclass column.  Once a match is found it grabs the memberof field in the matched line and splits it based on the semi-colon as the delimiter which produces a [string[]] of distinguished names of all the groups the account belonged to.  This array is passed to the pipeline and each DN is passed to the function Get-NameFromDN which uses the powerful -split operation to split the string based on a pattern ",*..=" (which will match ou= or ,ou= or cn= or ,dc= etc...).  The function then returns the second element of the split distinguished name (remember that because a DN starts with my delimiter pattern the first element [0] is empty).  The output produces a list of Group names only, that I pasted into an email and sent to my logical access guy.
I could have piped the group DNs to another function like Get-QADGroup $_, wrapped the function with parentheses, and gotten the name property like this:

...| %{(Get-QADGroup $_).name}

but that was significantly slower so I'd rather run it through my splitting function for speed sake.

You may have noticed in my Get-NameFromDN function I used the replace method.  That is because in my environment we use Lastname, Firstname.  LDAP has to escape the comma in the DN because each path element in the DN is separated by a comma; so commas within an attribute are escaped with a slash (\).  Additionally, csvde escapes the \, again so that it is \\, in the csv file.  The replace method in the function is replacing the escape character with nothing.  That way the list that is produced can be directly used in ADUC.

Of course this is just one example of how you can use the split functionality in PowerShell.  I'm sure that as you do more scripting the knowledge of the power of -split will come in handy.

Have fun.

Tuesday, February 9, 2010

Windows Pentesters Delight--Watch Out Linux Users, Move Over for MS.

Well one of my hobbies is security.  Of course anyone who wants the respect for the Security community should probably learn Linux and the tools that come with Linux.  This has led to me doing some cool stuff with Bash, learning BackTrack, and research and gaining knowledge of the Linux environment.  I've also been taking the course over at  I know that Linux is for serious security guru's (who would laugh at someone that said they use Windows for all their security needs), but I think that PowerShell has the ability to offer viable alternatives to some of the Linux tools.  Just throwing this out there--Linux should watch it or else it may lose it's 'Security' throne to Windows (I'm sure that comment is going to get flamed).

Anyway, take for example Information Gathering Techniques.  The BackTrack distro offers a number of tools to find user info and email info about companies from web sites.  So I was thinking, "How can I start converting some of these tools over to Windows using PowerShell?"  Well here is my start...

One of the first things I was thinking would be nice, would be to grab web pages to search for email addresses or other things.  So here is a quick little function for grabbing web pages:

function Get-WebPage{            
    $WebClient = New-Object System.Net.WebClient            
    $WebPage = $WebClient.DownloadData($Url)            
    return [System.Text.Encoding]::ASCII.GetString($WebPage)            

That little goody will get you the HTML text of the URL you supply.  "What can you do with that?" you ask.  How about getting Google results (if you get permission from Google first of course)...

function Get-GoogleResults{            
    Param(  [string]$Search,            
            [string]$SearchPrefix = "/search?q="            
    Function Get-ResultObject([string]$rTitle,[string]$rUrl){            
        $tempobject = New-Object PSObject -Property @{Title=$null;Link=$null}            
        $tempobject.Title = $rTitle            
        $tempobject.Link = $rUrl            
        return $tempobject            
    $PageNumber = ($PageNumber - 1) * 10            
    $WebSearch = "$site$searchprefix$search`&start=$PageNumber"             
    $regex = [regex]'<h3 class=r>.*?</h3>'            
    $GoogleResults = Get-WebPage $WebSearch            
    $regex.matches($GoogleResults) |             
        %{  $Title = [regex]::replace($_,"<.*?>","")            
            $Link = [regex]::matches($_,'\".*\"')| %{$_.value.trim('`"')}            
            $AllResults += @(Get-ResultObject $Title $Link)            
    return $AllResults            
The Get-GoogleResults will return the Title and link for each search result.  You can supply the page number to get the next page of results.  I.e. Get-GoogleResults 'beer lovers' 3 will retrieve the third page of results from a Google search.

That particular function returns an array of objects with Title and Link as properties.  You can then go through the array get the URL for each link and search it if you want for email addresses.

for($i=1;$i -le 10; $i++){Get-GoogleResults "" $i | %{Get-WebPage $_.Link | %{([regex]'\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*').matches($_) | %{$_.value}}}}

I'm pretty sure there's a couple of Linux tools that'll replace (LOL).

Have fun

Thursday, January 7, 2010

Get-Oneliner | Group it | Sort it | Format it

Well if you read my first post on my blog you will note that I'm not (or wasn't) a fan of one-liners.  I really like structured code; and PowerShell does a great job at letting me do that.  Recently, though, I've had a change of heart (the more I use PowerShell the more I change my views on it).  I've now officially become a fan of one-liners.

I've been reading Dr. Tobias Weltner's book Mastering-PowerShell (I find it to be very well written and super helpful).  Chapter 5 entitled "The PowerShell Pipeline" was very enlightening.  So I wrote a one-liner that gets all AD users and groups them by manager, sorts them and displays them in a nice text tree.  I have two versions.  I wrote the first one because I have a multi-domain forest and some of the mangers are not in the same domain as the users and currently the ActiveDirectory module won't let you search cross domain to my knowledge.  The second example is for a single domain forest.  Both one-liners take into consideration that the user object my not have the manager attribute set.

Example 1 Multi-domain Forest (replace foreigndomain and localdomain with real domain names)
Get-ADUser -filter * -Properties samaccountname, displayname, manager |             
    group {if(($_.manager -ne $null) -and ($_.manager -imatch "dc=localdomain")){            
        (Get-ADObject $_.manager -Properties displayname).displayname            
        if($_.manager -imatch "dc=foreigndomain"){"FOREIGNDOMAINNAME"            
        "UNKNOWN"}}} |             
        sort name |             
        %{$_.Name; "-------------------"; ($_.Group |             
            %{if($_.displayname -ne $null){"`t$($_.displayname)"}else{"`t$($_.samaccountname)"}}); "`n"}

Get-ADUser -filter * -Properties samaccountname, displayname, manager | group {if(($_.manager -ne $null) -and ($_.manager -imatch "dc=localdomain")){(Get-ADObject $_.manager -Properties displayname).displayname}else{if($_.manager -imatch "dc=foreigndomain"){"FOREIGNDOMAINNAME"}else{"UNKNOWN"}}} | sort name | %{$_.Name; "-------------------"; ($_.Group | %{if($_.displayname -ne $null){"`t$($_.displayname)"}else{"`t$($_.samaccountname)"}}); "`n"}            

Example 2 Single-domain Forest
Get-ADUser -filter * -Properties samaccountname, displayname, manager |             
    group {if($_.manager -ne $null){(Get-ADObject $_.manager -Properties displayname).displayname}else{"UNKNOWN"}} |            
    sort name |             
    % { $_.Name;             
        ($_.Group | %{if($_.displayname -ne $null){"`t$($_.displayname)"}else{"`t$($_.samaccountname)"}});            

Get-ADUser -filter * -Properties samaccountname, displayname, manager | group {if($_.manager -ne $null){(Get-ADObject $_.manager -Properties displayname).displayname}else{"UNKNOWN"}} | sort name | % { $_.Name;"-------------------";($_.Group | %{if($_.displayname -ne $null){"`t$($_.displayname)"}else{"`t$($_.samaccountname)"}});"`n"}

Output will be something like this:

PS C:\>Get-ADUser -filter * -Properties samaccountname, displayname, manager | group {if(($_.manager -ne $null) -and ($_.manager -imatch "dc=localdomain")){(Get-ADObject $_.manager -Properties displayname).displayname}else{if($_.manager -imatch "dc=foreigndomain"){"FOREIGNDOMAINNAME"}else{"UNKNOWN"}}} | sort name | %{$_.Name; "-------------------"; ($_.Group | %{if($_.displayname -ne $null){"`t$($_.displayname)"}else{"`t$($_.samaccountname)"}}); "`n"}            

Foreign Domain




You get the idea.

Have fun.!

Monday, January 4, 2010

I definately recommend the PowerShellPack.

When I first started developing for PowerShell I was using the PowerShell ISE that comes with Windows 7.  Mainly because my version of Sapien PrimalScript doesn't support PowerShell v2 with Intellisense.  So I looked all over for a snazzy IDE that gave me full support for PS 2.  I liked Quest's editor (PowerGUI) the best, but it just doesn't work at present with the AD module from Microsoft (go figure--Quest has their own AD tools, which are very robust).  So I checked out Idera's PowerShellPlus.  It is just a bit much for me.  Don't get me wrong.  PowerShellPlus is really amazing and Dr. Tobias Weltner has a really good book (and it's free) to help learn PowerShell, but the IDE is just too much for me (price wise-at least for now).  Also, Sapien's PrimalScript 2009 is supposed to be very good (I've been using 2007 for all my VBScripting for years now and I really like their IDE).

However, the more I used PowerShell ISE the more I enjoyed developing with it. It has a great interactive environment.  I can try out my commands first in the console window then go on the left pane (where I've located the editor in my environment), and type my code when it finally works. It's so simple, yet so powerful.

But...I want intellisense (so I thought).  Also I'm blogging now, and I want to copy my nice color coded script to my blog.  Well there really isn't a need for intellisense in ISE. Microsft allows the script editor to use tab completion (out-of-the-box).  I love tab completion and behaves just like the PS console.  Once you get used to tab completion intellisense becomes annoying (at least in my case it did).

What about my blogging though. Well that is where the PowerShellPack comes in (finally--maybe I should have titled this blog entry something else).  When you install the pack (get it here) it adds a ton of features to the ISE.  I can now copy my code in colored HTML straight to the clipboard so that I can post it in my blog.  Isn't that great!

That's not all the PowerShellPack does.  It comes with 10 modules that let you do all kinds of things.  I'm playing around with building UIs using the new cmdlets around WPF.  Good stuff.  Head over to the main site.  Check out some videos and have fun.

Running PowerShell v2 RTM on Something Other Than Win 7 or Win 2008R2

I know this site is dedicated to running PowerShell v2 RTM on Windows 7 or Windows 2008 R2.  However if you would like to start playing with PowerShell v2 RTM on some other operating system go here.

Have fun.