2013 PowerShell Scripting Games Advanced Event 4 – Auditors Love Greenbar Paper

For this event I created multiple functions and I'm going to quote chapter 6, section 1 of the Learn PowerShell Toolmaking in a Month of Lunches book written by Don Jones and Jeffery Hicks, published by Manning:

A function should do one and only one of these things:

  • Retrieve data from someplace
  • Process data
  • Output data to some place
  • Put data into some visual format meant for human consumption

I started out by naming my function to create the audit report properly (New-UserAuditReport). If you having trouble picking a name for your function, it's probably because you've broken the previous rule and your function does more than one thing.

Of course, all of the functions (4 of them) all have their own comment based help.

Here's the validation that I performed on the filename and extension via a regular expression. This not only enforces the htm and html file extension specified in the requirements, but also ensures that the file name is valid on a Windows system. There's no sense in allowing the script to process all the way to the end only to have it fail because the file name is not valid on a Windows system. Be proactive, not reactive. This MSDN article explains what is not valid in a Windows filename.

sq2013-event4a.png

I couldn't decide whether to use the cmdlets that are part of the Active Directory module or not because they may not be present on the system this is being run from and they only work on Windows Server 2008 R2 or higher unless you have a list of ingredients to include a Leprechaun as discussed in this TechNet blog to make them work with Windows Server 2003 domain controllers. Here's a "Hey, Scripting Guy! blog" on the same subject that refers to 2008 non-R2 domain controllers as well. The problem is that since the scenario didn't specify what OS the domain controllers are running, these cmdlets may not work.

Using ADSI (Active Directory Service Interfaces) on the other hand would be something that would work on systems that didn't have the AD PowerShell module installed and it would also work against older domain controllers.

What I decided is why make a decision? Why not have the best of both worlds? Try the AD cmdlets and I mean literally try the AD cmdlets as shown in the first try catch block in the code below and then if there's an issue, try ADSI as shown in the second (nested) try catch block in the code shown below as well:

sq2013-event4b.png

If neither one works then the script exits with a warning instead of continuing to only error out somewhere further along in the code. That's what the problem variable is for.

sq2013-event4c.png

There is an issue on the Scripting Games website where the "Plain" tab doesn't show the inline CSS so be sure to use the "Code" tab when copying and pasting the code to use on your own machine.

I figured those auditors would love some old school Greenbar paper. Here's an example of six results showing a "Greenbar" style webpage (aka virtual Greenbar paper):

sq2013-event4h.png

You may be wondering why I would chose to use the LastLogonTimestamp property when there are other properties such as "LastLogon" that already have human readable dates to start with? That's because the LastLogon property is not replicated to all domain controllers in the domain and LastLogonTimestamp is beginning with Windows Server 2003. So it's either query all the domain controllers in the domain or convert the date. From an efficiency standpoint it's better to convert the date rather than possibly querying hundreds of domain controllers. See this TechNet blog article for more information about those two properties.

sq2013-event4d.png

Now for the two functions that Get the data. The first is Get-ADRandomUser:

sq2013-event4e.png

As you can see, this is a reusable function. Here's the output that it creates (which is objects):

sq2013-event4i.png

The ADSI one was a little trickier because I needed it to return the same data as the AD one. This one is called Get-ADSIRandomUser:

sq2013-event4f.png

Although the type of object that this function produces is different, it's still objects and it produces the same output from a properties and data standpoint as the previous function:

sq2013-event4j.png

And finally, there is the "Set-AlternatingCSS" function which is a modified version of a function from Don Jones's "Creating HTML Reports in PowerShell" book.

sq2013-event4g.png

Update 02/09/14:

The link to my solution is no longer valid so I’m posting it here since I’ve received some requests for it:

  1#Requires -Version 3.0
  2function New-UserAuditReport {
  3
  4<#
  5.SYNOPSIS
  6Creates a report of specific properties for random Active Directory users in HTM or HTML format to be used for
  7auditing purposes.
  8.DESCRIPTION
  9    New-UserAuditReport is a function that creates a htm or html report for a random number of active directory
 10user accounts. The specific information that is on the report is: user name, department, title, date
 11and time for the last interactive, network, or service logon, date and time of last password change, and
 12whether or not the account is disabled or locked out. The ActiveDirectory PowerShell module is not required,
 13but it is attempted first via an external reusable function and then ADSI is attempted via another reusable
 14external function if this function experiences an issue with the Active Directory PowerShell module.
 15    The LastLogonTimestamp property was chosen because the other possible attributes are bot replicated to all
 16the domain controllers in the domain and each one would need to be queried as discussed in this blog article:
 17http://blogs.technet.com/b/askds/archive/2009/04/15/the-lastlogontimestamp-attribute-what-it-was-designed-for-and-how-it-works.aspx
 18    The pwdLastSet property was chosen to provide consistency on what is returned by Get-ADUser and ADSI.
 19.PARAMETER Records
 20The number of random Active Directory user records to retrieve.
 21.PARAMETER Path
 22The folder or directory to place the htm or html file in. The default is the current user's temporary directory.
 23.PARAMETER FileName
 24The file name that will be used to save the report as. Only valid file names with a .htm or .html file extension
 25are accepted.
 26.PARAMETER Force
 27Switch parameter that when specified overwrites the destination file if it already exists.
 28.EXAMPLE
 29New-UserAuditReport -FileName AuditReport.htm
 30.EXAMPLE
 31New-UserAuditReport -Records 10 -FileName AuditReport.html
 32.EXAMPLE
 33New-UserAuditReport -Records 15 -Path c:\tmp -FileName MyAuditReport.htm
 34.EXAMPLE
 35New-UserAuditReport -Records 25 -Path c:\tmp -FileName MyAuditReport.htm -Force
 36.INPUTS
 37None
 38.OUTPUTS
 39None
 40#>
 41
 42    [CmdletBinding()]
 43    param(
 44        [ValidateNotNullorEmpty()]
 45        [int]$Records = 20,
 46
 47        [ValidateNotNullorEmpty()]
 48        [ValidateScript({Test-Path $_ -PathType 'Container'})]
 49        [string]$Path = $Env:TEMP,
 50
 51        [Parameter(Mandatory=$True)]
 52        [ValidatePattern("^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\"";|/]+\.html?$")]
 53        [string]$FileName,
 54
 55        [switch]$Force
 56    )
 57
 58    $Params = @{
 59        Records = $Records
 60        ErrorAction = 'Stop'
 61        ErrorVariable = 'Issue'
 62    }
 63
 64    $Problem = $false
 65
 66    Try {
 67        Write-Verbose -Message "Attempting to use the Active Directory PowerShell Module"
 68        $Users = Get-ADRandomUser @Params
 69    }
 70    catch {
 71
 72        try {
 73            Write-Verbose -Message "Failure when attempting to use the Active Directory PowerShell Module, now attempting to use ADSI"
 74            $Users = Get-ADSIRandomUser @Params
 75        }
 76        catch {
 77            $Problem = $True
 78            Write-Warning -Message "$Issue.Exception.Message"
 79        }
 80
 81    }
 82
 83    If (-not($Problem)) {
 84
 85Write-Verbose -Message "Defining GreenBar CSS Style"
 86$GreenBarStyle = @"
 87    <style>
 88    body {
 89        color:#333333;
 90        font-family:"Lucida Grande", verdana, sans-serif;
 91        font-size: 10pt;
 92    }
 93    h1 {
 94        text-align:center;
 95    }
 96    h2 {
 97        border-top:2px solid #4e9a06;
 98    }
 99
100    th {
101        font-weight:bold;
102        color:#eeeeee;
103        background-color:#4e9a06;
104    }
105    .odd  { background-color:#ffffff; }
106    .even { background-color:#e4ffc7; }
107    </style>
108"@
109
110        Write-Verbose -Message "Defining auditor friendly dates, property names, converting to HTMl fragment, and applying CSS."
111        $UsersHTML = $Users | Select-Object -Property @{
112                                 Label='UserName';Expression ={$_.samaccountname}},
113                                 Department,
114                                 Title,
115                                 @{Label='LastLogin';Expression ={([datetime]::fromfiletime([int64]::Parse($_.lastlogontimestamp)))}},
116                                 @{Label='PasswordLastChanged';Expression ={([datetime]::fromfiletime([int64]::Parse($_.pwdlastset)))}},
117                                 @{Label='IsDisabled';Expression ={(-not($_.enabled))}},
118                                 @{Label='IsLockedOut';Expression ={$_.lockedout}} |
119                              ConvertTo-Html -Fragment |
120                              Out-String |
121                              Set-AlternatingCSS -CSSEvenClass 'even' -CssOddClass 'odd'
122
123        $HTMLParams = @{
124            'Head'="<title>Random User Audit Report</title>$GreenBarStyle"
125            'PreContent'="<H2>Random User Audit Report for $Records Users</H2>"
126            'PostContent'= "$UsersHTML <HR> $(Get-Date)"
127        }
128
129        $Params.Remove("Records")
130
131        Try {
132            Write-Verbose -Message "Attempting to build filepath. The regular expression in the params block validated that the file name is valid on a windows system."
133            $FilePath = Join-Path -Path $Path -ChildPath "$($FileName.ToLower())" @Params
134
135            Write-Verbose -Message "Converting to HTML and creating the file."
136            ConvertTo-Html @HTMLParams @Params |
137            Out-File -FilePath $FilePath -NoClobber:(-not($Force)) @Params
138        }
139        catch {
140            Write-Warning -Message "Use the -Force parameter to overwrite the existing file. Error details: $Issue.Message.Exception"
141        }
142    }
143
144}
145
146function Get-ADRandomUser {
147#Requires -Modules ActiveDirectory
148
149<#
150.SYNOPSIS
151Returns a list of specific properties for random Active Directory users using the Active Directory PowerShell Module.
152.DESCRIPTION
153Get-ADRandomUser is a function that retrieves a list of random active directory users using the Active Directory
154PowerShell module. The results are returned as objects for reusability.
155.PARAMETER Records
156The number of random Active Directory user records to retrieve.
157.EXAMPLE
158Get-ADRandomUser -Records 20
159.INPUTS
160None
161.OUTPUTS
162Selected.Microsoft.ActiveDirectory.Management.ADUser
163#>
164
165    [CmdletBinding()]
166    param(
167        [Parameter(Mandatory=$True)]
168        [int]$Records
169    )
170
171    $Params = @{
172        Filter = '*'
173        Properties = 'SamAccountName',
174                     'Department',
175                     'Title',
176                     'LastLogonTimestamp',
177                     'pwdLastSet',
178                     'Enabled',
179                     'LockedOut'
180        ErrorAction = 'Stop'
181        ErrorVariable = 'Issue'
182    }
183
184    try {
185        Write-Verbose -Message "Attempting to import the Active Directory Get-ADUser cmdlet."
186        Import-Module -Name ActiveDirectory -Cmdlet Get-ADUser -ErrorAction Stop -ErrorVariable Issue
187
188        Write-Verbose -Message "Attempting to query Active Directory using the Get-ADUser cmdlet"
189        $RandomUsers = Get-ADUser @Params |
190                       Get-Random -Count $Records
191
192        $Params.Property = $Params.Properties
193        $Params.Remove("Properties")
194        $Params.Remove("Filter")
195
196        $RandomUsers | Select-Object @Params
197
198    }
199    catch {
200        Write-Warning -Message "$Issue.Message.Exception"
201    }
202
203}
204
205function Get-ADSIRandomUser {
206#Requires -Version 3.0
207
208<#
209.SYNOPSIS
210Returns a list of specific properties for random Active Directory users using ADSI.
211.DESCRIPTION
212Get-ADSIRandomUser is a function that retrieves a list of random active directory users using ADSI (Active Directory
213Service Inferfaces). The results are returned as objects for reusability. This function does not depend on the
214Active Directory PowerShell module.
215.PARAMETER Records
216The number of random Active Directory user records to retrieve.
217.EXAMPLE
218Get-ADSIRandomUser -Records 20
219.INPUTS
220None
221.OUTPUTS
222System.Management.Automation.PSCustomObject
223#>
224
225    [CmdletBinding()]
226    param(
227        [Parameter(Mandatory=$True)]
228        [int]$Records
229    )
230
231    try {
232        $searcher=[adsisearcher]'(&(objectCategory=user)(objectClass=user))'
233
234        $props=@(
235            'samaccountname',
236            'department',
237            'title',
238            'lastlogontimestamp',
239            'pwdlastset',
240            'useraccountcontrol',
241            'islockedout'
242        )
243
244        $searcher.PropertiesToLoad.AddRange($props)
245        $RandomUsers = $searcher.FindAll() |
246                       Get-Random -Count $Records
247    }
248    catch {
249        Write-Warning -Message "Error retrieving active directory user information using ADSI"
250    }
251
252    foreach ($user in $RandomUsers) {
253
254        [pscustomobject][ordered]@{
255            SamAccountName = $($user.Properties.samaccountname)
256            Department = $($user.Properties.department)
257            Title=$($user.Properties.title)
258            LastLogonTimestamp=$($user.Properties.lastlogontimestamp)
259            pwdLastSet=$($user.Properties.pwdlastset)
260            Enabled = (-not($($user.GetDirectoryEntry().InvokeGet('AccountDisabled'))))
261            LockedOut = $($user.GetDirectoryEntry().InvokeGet('IsAccountLocked'))
262        }
263
264    }
265
266}
267
268function Set-AlternatingCSS {
269
270<#
271.SYNOPSIS
272Setup an alternating cascading style sheet.
273.DESCRIPTION
274The Set-AlternatingCSS function is a modified version of a function from Don Jones's "Creating HTML Reports in PowerShell"
275book. Visit http://powershellbooks.com for details about this free ebook (Thank You Don!).
276.PARAMETER HTMLFragment
277The HTML fragment created with the ConvertTo-Html cmdlet that you wish to apply the alternating CSS to.
278.PARAMETER CSSEvenClass
279The CSS to apply to the even rows within the table.
280.PARAMETER CssOddClass
281The CSS to apply to the odd rows within the table.
282.EXAMPLE
283Set-AlternatingCSS -HTMLFragment ('My HTML' | ConvertTo-Html -Fragment | Out-String) -CSSEvenClass 'even' -CssOddClass 'odd'
284.EXAMPLE
285'My HTML' | ConvertTo-Html -Fragment | Out-String |Set-AlternatingCSS -CSSEvenClass 'even' -CssOddClass 'odd'
286.INPUTS
287String
288.OUTPUTS
289String
290#>
291
292    [CmdletBinding()]
293    param(
294        [Parameter(Mandatory=$True,
295                   ValueFromPipeline=$True)]
296        [string]$HTMLFragment,
297
298        [Parameter(Mandatory=$True)]
299        [string]$CSSEvenClass,
300
301        [Parameter(Mandatory=$True)]
302        [string]$CssOddClass
303    )
304
305    [xml]$xml = $HTMLFragment
306    $Table = $xml.SelectSingleNode('table')
307    $Classname = $CSSOddClass
308
309    foreach ($tr in $Table.tr) {
310        if ($Classname -eq $CSSEvenClass) {
311            $Classname = $CssOddClass
312        } else {
313            $Classname = $CSSEvenClass
314        }
315        $Class = $xml.CreateAttribute('class')
316        $Class.value = $Classname
317        $tr.attributes.append($Class) | Out-Null
318    }
319
320    $xml.innerxml | Out-String
321}

µ