Powershell Code That Literally Writes Itself - Automaception

I amused myself with Powershell today and thought I'd share. I might have also named today's post, "Write-Host does have a use after all!"

Today, I had the task of synchronizing the country attributes of thousands of users from a non-Microsoft LDAP server into multiple Active Directories.  This non-Microsoft LDAP server stored the country attribute ("c" in LDAP parlance) of each user as an ISO 3166 Alpha-3 three-letter abbreviation.  I wanted to convert that into the ISO 3166 alpha-2 two-letter notation before I imported it into Active Directory, as well as fill out the rest of the country-related attributes at the same time.

As most AD administrators know, when you want to programmatically set a user's country, you have to make the change in three different places if you want to be thorough.  You have to change co (Text-Country,) c (Country-Name,) and countryCode (Country-Code.)  Those three fields are a free-form text entry, an ISO-3166 A2 or A3 abbreviation, and a numeric value, respectively.

So the first thing I do is track down the ISO 3166 list.  It looks like this:

AALAND ISLANDS                                  AX      ALA     248
AFGHANISTAN                                     AF      AFG     004
ALBANIA                                         AL      ALB     008
ALGERIA                                         DZ      DZA     012
...

And on and on... for 240 countries.

I was thinking I'd want a Switch statement in my script... the idea is that I Switch($x) where $x is the three-letter country abbreviation that came from the LDAP server. Visions flashed through my mind of me staying up all night writing this monotonous switch block with 240 cases in it.  And then I thought, "Hey, Powershell is the most powerful automation framework there is. Surely I can use it to automate the automation!"  And so I set out to have my Powershell script literally write itself.

First, save that ISO 3166 country code list to a text file. That the list is already in a fixed-width format is going to make this extremely simple.

$Countries = Get-Content C:\Users\Ryan\Desktop\3166.txt

Write-Host "Switch (`$Transaction.NewValue.ToUpper().Trim())" #E.g. 'USA' or  'BEL'
Write-Host "{"
Foreach ($Line In $Countries)
{
    [String]$CountryCode = $Line[64] + $Line[65] + $Line[66]
    [String]$ThreeLetter = $Line[56] + $Line[57] + $Line[58]
    [String]$TwoLetter   = $Line[48] + $Line[49]
    [String]$FreeForm    = $Line.Substring(0, 46).Trim().Replace("'", $Null)

    Write-Host "    '$ThreeLetter'"
    Write-Host "    {"
    Write-Host "        Set-ADUser -Identity `$Transaction.ObjectGUID -Replace @{ countryCode = $CountryCode } -ErrorAction Stop"
    Write-Host "        Set-ADUser -Identity `$Transaction.ObjectGUID -Replace @{ co = '$FreeForm' } -ErrorAction Stop"
    Write-Host "        Set-ADUser -Identity `$Transaction.ObjectGUID -Replace @{ c = '$TwoLetter' } -ErrorAction Stop"
    Write-Host "    }"
}
Write-Host "    Default { Write-Error `"`$(`$Transaction.NewValue) was not recognized as a country.`" }"
Write-Host "}"

When I ran the above script, it printed out all the code for my massive switch block, so that all I had to do was copy the text out of the Powershell window, and paste it into the middle of the script I was working on.  It came out looking like this:

Switch ($Transaction.NewValue.ToUpper().Trim()) #E.g. 'USA' or  'BEL'
{
    'ALA'
    {
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ countryCode = 248 } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ co = 'AALAND ISLANDS' } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ c = 'AX' } -ErrorAction Stop
    }
    #
    # ... 238 more countries ...
    #
    'ZWE'
    {
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ countryCode = 716 } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ co = 'ZIMBABWE' } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ c = 'ZW' } -ErrorAction Stop
    }
    Default { Write-Error "$($Transaction.NewValue) was not recognized as a country." }
}

And there we have it.  Now I don't have any employees from Burkina Faso, but it's nice to know that I'd be able to classify them if I did.  Now with all the time I saved myself from having to type all that out by hand, I figured I'd write a blog entry about it.

Override Write-Debug to Avoid 80-Character Word-Wrapping in Transcript

I was writing some long, complicated Powershell scripts recently, and complicated scripts call for plenty of tracing/debug logging.  These scripts were to be run as scheduled tasks, and it was important that I saved this debug logging information to a file, so I figured that instead of re-inventing the wheel, I'd just use Start-Transcript in conjunction with Write-* cmdlets to automatically capture all the output.

This almost worked fine, except that the lines in the resulting transcript file were word-wrapped at 80 characters.  This made the transcripts ugly and hard to read.  The only thing worse than reading debug logs while trying to diagnose a subtle bug is having to do it when the debug logs are ugly and hard to read.

Hm, would it help if I resized the Powershell host window and buffer at the beginning of the script, like so? 

$Host.UI.RawUI.BufferSize = New-Object System.Management.Automation.Host.Size 180, 3000
$Host.UI.RawUI.WindowSize = New-Object System.Management.Automation.Host.Size 180, 50

No, that did not help.

As it turns out, the Write-Host cmdlet does not exhibit the same word-wrapping limitation as its Write-Debug and Write-Warning brethren.  So instead of changing the 900 Write-Debug cmdlets already in our script, let's just "override" the Write-Debug cmdlet:

Function Global:Write-Debug([Parameter(Mandatory=$True, ValueFromPipeline=$True)][String]$Message)
{
    If ($DebugPreference -NE 'SilentlyContinue')
    {
        Write-Host "DEBUG: $Message" -ForegroundColor 'Yellow'
    }
}

Ah, no more needless word wrapping in my transcripts now.  Just be careful about putting stuff in the Global scope if you're working with other actors in the same environment.

Powershell: If You Want to Catch, Don't Forget to Throw

I made a Powershell scripting error the other day, and wanted to share my lesson learned.  In hindsight it seems obvious, but I made the mistake nevertheless, then cursed when my script presented with an apparent bug.

I had a script that iterated through a collection of items using a ForEach loop - an extremely common task in Powershell.  For each iteration of that ForEach loop, I performed a function that had a chance of failing, so I wrapped it in a Try/Catch block.  The rest of the ForEach loop below that performed further processing that depended on the success of the preceding function, so if the previous function failed, I wanted to skip the rest of that iteration and use the Continue statement to skip directly to the next ForEach iteration.

Let me illustrate:

:NextUser ForEach ($User In $Users)
{
    Try
    {
        Write-Debug "I'm about to try something risky..."
        Pre-Process $User
    }
    Catch
    {
        Write-Error $_.Exception        
        Continue NextUser
    }
    Write-Debug "Since pre-processing for $User was successful, I'm going to do more stuff here..."
    Post-Processing $User
}

And the Pre-Process function looked like this:

Function Pre-Process ($User)
{
    Try
    {
        [System.Some.Static]::Method($User)
    }
    Catch
    {
        Write-Error $_.Exception
    }
}

So what actually ended up happening was when the "Pre-Processing" function failed, it flashed an error message on the screen... but then the rest of the ForEach iteration continued to execute in an unpredictable and undesired way.  Why did my Continue statement in the Catch block not take me back to the next ForEach iteration?

Well as it turns out, the Catch block never ran.  Just because you catch an exception and use the Write-Error cmdlet doesn't mean you've thrown a terminating error to the caller.  And you need a terminating error in order to exit a Try block and go into a Catch block.  Since I was using a custom function, ErrorAction -Stop and $ErrorActionPreference didn't do anything for me. What I needed to do was use Throw to generate the needed terminating error.  What was throwing me off (no pun intended) was that I was confused as to which one of the Write-Error statements was actually taking place.  The one in the custom function, or the one in the ForEach loop that contained a call to the custom function?

In retrospect, I should probably stop using Try/Catch blocks altogether in custom Powershell functions.  You can easily suppress a needed exception with a Try/Catch and cause unintended bugs.

But for now, simply adding a Throw statement to my custom function was all I needed to activate the Catch block in the calling script:

Function Pre-Process ($User)
{
    Try
    {
        [System.Some.Static]::Method($User)
    }
    Catch
    {
        Write-Error $_.Exception
        Throw $_.Exception
    }
}

Be the Master of the LastLogonTimestamp Attribute with S4U2Self


I've written a little bit about the LastLogonTimestamp/LastLogonDate attribute here, and of course there is AskDS's notorious article on the subject here, but today I'm going to give you a handy little tip that I don't think I have mentioned before.

If you're an Active Directory administrator, chances are you're interested or have been interested in knowing if a given account is "stale," meaning that the account's owner has not logged in to the domain in some time.  (Keep in mind that an account could be either a user or a computer as it relates to Active Directory.)  You, like many sysadmins, might have some script or automated process that checks for stale accounts using a command-line approach, such as:

dsquery user -inactive 10

or Powershell's extremely flexible:

Get-ADUser -Filter * -Properties LastLogonDate
| ? { $_.Enabled -AND $_.LastLogonDate -LT (Get-Date).AddDays(-90) }

And then you take action on those inactive accounts, such as moving them to an "Inactive Users" OU, or disabling their accounts, or sending a reminder email to the account holder reminding them that they have an account in this domain, etc.

It might be handy for you to "artificially" update the lastLogonTimeStamp of another user though.  Maybe you know that this user is on vacation and you don't want their user account to get trashed by the "garbage collector" for being inactive.  According to the documentation, lastLogonTimeStamp is only editable by the system, so forget about directly modifying the attribute the way that you would other LDAP attributes.  And of course "LastLogonDate" is not a real attribute at all - merely a calculated attribute that Powershell gives you to be helpful by converting lastLogonTimestamp into a friendly .NET DateTime object.

The S4U2Self (Service for User to Self) Kerberos extension can help us here.

Just right click on any object such as an OU in Active Directory a folder in a file share, go to its Properties, then the Security tab.  Click the Advanced button.  Now go to the Effective Permissions tab.  Click the Select... button, and choose the user whose lastLogonTimestamp you want to update.  We are going to calculate the effective permissions of this inactive user:

By doing this, you are invoking the S4U2Self Kerberos extension, whereby the system will "go through the motions of Kerberos authentication and obtain a logon for the client, but without providing the client's credentials. Thus, you're not authenticating the client in this case, only making the rounds to collect the group security identifiers (SIDs) for the client."[1]

And just like that, you have updated the "Last Logon Time" on another user's behalf, without that user having to actually log on themselves.

Writing and Distributing a Powershell Module via GPO

I recently wrote a bunch of Powershell Cmdlets that were related to each other, so I decided to package them all up together as a Powershell module.  These particular Cmdlets were tightly integrated with Active Directory, so it made sense that they would often be run on a domain controller, or against a domain controller, usually by a domain administrator.

First, to create a Powershell script module, you create a folder, and then place your *.psm1 and *.psd1 files in that folder and they must have exactly the same name as that folder. (You can also compile your Powershell module into a DLL, but let's just talk about script modules today.)  So for instance, if you name your module "CloudModule," you would create a directory such as:

C:\Program Files\PSModules\CloudModule\

And then you'd place your files of the same name in that folder:

C:\Program Files\PSModules\CloudModule\CloudModule.psm1
C:\Program Files\PSModules\CloudModule\CloudModule.psd1

I think that a subdirectory of Program Files works well, because the Program Files directory is protected from modification by non-administrators and low-integrity processes.

The psd1 file is the manifest for the PS module. You can easily create a new manifest template with the New-ModuleManifest cmdlet, and customize the template to suit your needs. The module's manifest is simply the "metadata" for the Powershell module, such as what version of Powershell it requires, other prerequisite modules it requires, the module's author, etc. The cool thing is that with current versions of Powershell, just by saying:

RequiredModules = @('ActiveDirectory')

in your manifest file, Powershell will automatically load the ActiveDirectory module for you the first time any cmdlet from your module is run. So you really don't need to put an Import-Module or a #Requires -Module anywhere.

The psm1 file is the collection of Powershell Advanced Functions (cmdlets) that make up your module. I would recommend naming all of your cmdlets with a common theme, using supported verbs (Get-Verb) and a common prefix. You know how the Active Directory module does Get-ADUser, Remove-ADObject, Set-ADGroup, etc.? You should do that too. For example, if you work for McDonald's, do Show-McSalary, Deny-McWorkersComp, Stop-McStrike, etc.

Now that you've got your module directory and files laid out, you need to add that path to your PSModulePath environment variable so that Powershell knows where to look for it every time it starts. Do not try to cheat and put your custom module in the same System32 directory where Microsoft puts their standard PS modules.

I decided that I wanted all my domain controllers to automatically load this custom module. And I don't want to go install and maintain this this thing on 50 separate machines.  So for a centrally-managed solution, let's utilize Group Policy and SYSVOL.

First, let's put something like this in the Default Domain Controllers (or equivalent) GPO:


(*Click to enlarge*)

And secondly, let's put our Powershell module in:

C:\Windows\SYSVOL\sysvol\Contoso.com\scripts\PSModules\CloudModule\

Of course, since this directory is replicated amongst all domain controllers, we only need to do this on one domain controller for the files to become available on all domain controllers.  Likewise, any time you update the module, you only need to update it on one DC, and all DCs will get the update.

Lastly, since this Powershell module is only for Domain Administrators, and SYSVOL by design is a very public place, let's protect our module's directory from prying eyes:

Careful that you only modify the permissions of that one specific module directory... you don't want to modify the permissions of anything else in Sysvol.