The requirements for the 2013 Scripting Games Advanced Event 4 can be found here. 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.
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:
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.
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):
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.
Now for the two functions that “Get” the data. The first is “Get-ADRandomUser”:
As you can see, this is a reusable function. Here’s the output that it creates (which is objects):
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”:
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:
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. Visit PowerShellBooks.com for details about this free ebook (Thank You Don!)
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | #Requires -Version 3.0 function New-UserAuditReport { <# .SYNOPSIS Creates a report of specific properties for random Active Directory users in HTM or HTML format to be used for auditing purposes. .DESCRIPTION New-UserAuditReport is a function that creates a htm or html report for a random number of active directory user accounts. The specific information that is on the report is: user name, department, title, date and time for the last interactive, network, or service logon, date and time of last password change, and whether or not the account is disabled or locked out. The ActiveDirectory PowerShell module is not required, but it is attempted first via an external reusable function and then ADSI is attempted via another reusable external function if this function experiences an issue with the Active Directory PowerShell module. The LastLogonTimestamp property was chosen because the other possible attributes are bot replicated to all the domain controllers in the domain and each one would need to be queried as discussed in this blog article: http://blogs.technet.com/b/askds/archive/2009/04/15/the-lastlogontimestamp-attribute-what-it-was-designed-for-and-how-it-works.aspx The pwdLastSet property was chosen to provide consistency on what is returned by Get-ADUser and ADSI. .PARAMETER Records The number of random Active Directory user records to retrieve. .PARAMETER Path The folder or directory to place the htm or html file in. The default is the current user's temporary directory. .PARAMETER FileName The file name that will be used to save the report as. Only valid file names with a .htm or .html file extension are accepted. .PARAMETER Force Switch parameter that when specified overwrites the destination file if it already exists. .EXAMPLE New-UserAuditReport -FileName AuditReport.htm .EXAMPLE New-UserAuditReport -Records 10 -FileName AuditReport.html .EXAMPLE New-UserAuditReport -Records 15 -Path c:\tmp -FileName MyAuditReport.htm .EXAMPLE New-UserAuditReport -Records 25 -Path c:\tmp -FileName MyAuditReport.htm -Force .INPUTS None .OUTPUTS None #> [CmdletBinding()] param( [ValidateNotNullorEmpty()] [int]$Records = 20, [ValidateNotNullorEmpty()] [ValidateScript({Test-Path $_ -PathType 'Container'})] [string]$Path = $Env:TEMP, [Parameter(Mandatory=$True)] [ValidatePattern("^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\"";|/]+\.html?$")] [string]$FileName, [switch]$Force ) $Params = @{ Records = $Records ErrorAction = 'Stop' ErrorVariable = 'Issue' } $Problem = $false Try { Write-Verbose -Message "Attempting to use the Active Directory PowerShell Module" $Users = Get-ADRandomUser @Params } catch { try { Write-Verbose -Message "Failure when attempting to use the Active Directory PowerShell Module, now attempting to use ADSI" $Users = Get-ADSIRandomUser @Params } catch { $Problem = $True Write-Warning -Message "$Issue.Exception.Message" } } If (-not($Problem)) { Write-Verbose -Message "Defining GreenBar CSS Style" $GreenBarStyle = @" <style> body { color:#333333; font-family:"Lucida Grande", verdana, sans-serif; font-size: 10pt; } h1 { text-align:center; } h2 { border-top:2px solid #4e9a06; } th { font-weight:bold; color:#eeeeee; background-color:#4e9a06; } .odd { background-color:#ffffff; } .even { background-color:#e4ffc7; } </style> "@ Write-Verbose -Message "Defining auditor friendly dates, property names, converting to HTMl fragment, and applying CSS." $UsersHTML = $Users | Select-Object -Property @{ Label='UserName';Expression ={$_.samaccountname}}, Department, Title, @{Label='LastLogin';Expression ={([datetime]::fromfiletime([int64]::Parse($_.lastlogontimestamp)))}}, @{Label='PasswordLastChanged';Expression ={([datetime]::fromfiletime([int64]::Parse($_.pwdlastset)))}}, @{Label='IsDisabled';Expression ={(-not($_.enabled))}}, @{Label='IsLockedOut';Expression ={$_.lockedout}} | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSS -CSSEvenClass 'even' -CssOddClass 'odd' $HTMLParams = @{ 'Head'="<title>Random User Audit Report</title>$GreenBarStyle" 'PreContent'="<H2>Random User Audit Report for $Records Users</H2>" 'PostContent'= "$UsersHTML <HR> $(Get-Date)" } $Params.Remove("Records") Try { 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." $FilePath = Join-Path -Path $Path -ChildPath "$($FileName.ToLower())" @Params Write-Verbose -Message "Converting to HTML and creating the file." ConvertTo-Html @HTMLParams @Params | Out-File -FilePath $FilePath -NoClobber:(-not($Force)) @Params } catch { Write-Warning -Message "Use the -Force parameter to overwrite the existing file. Error details: $Issue.Message.Exception" } } } function Get-ADRandomUser { #Requires -Modules ActiveDirectory <# .SYNOPSIS Returns a list of specific properties for random Active Directory users using the Active Directory PowerShell Module. .DESCRIPTION Get-ADRandomUser is a function that retrieves a list of random active directory users using the Active Directory PowerShell module. The results are returned as objects for reusability. .PARAMETER Records The number of random Active Directory user records to retrieve. .EXAMPLE Get-ADRandomUser -Records 20 .INPUTS None .OUTPUTS Selected.Microsoft.ActiveDirectory.Management.ADUser #> [CmdletBinding()] param( [Parameter(Mandatory=$True)] [int]$Records ) $Params = @{ Filter = '*' Properties = 'SamAccountName', 'Department', 'Title', 'LastLogonTimestamp', 'pwdLastSet', 'Enabled', 'LockedOut' ErrorAction = 'Stop' ErrorVariable = 'Issue' } try { Write-Verbose -Message "Attempting to import the Active Directory Get-ADUser cmdlet." Import-Module -Name ActiveDirectory -Cmdlet Get-ADUser -ErrorAction Stop -ErrorVariable Issue Write-Verbose -Message "Attempting to query Active Directory using the Get-ADUser cmdlet" $RandomUsers = Get-ADUser @Params | Get-Random -Count $Records $Params.Property = $Params.Properties $Params.Remove("Properties") $Params.Remove("Filter") $RandomUsers | Select-Object @Params } catch { Write-Warning -Message "$Issue.Message.Exception" } } function Get-ADSIRandomUser { #Requires -Version 3.0 <# .SYNOPSIS Returns a list of specific properties for random Active Directory users using ADSI. .DESCRIPTION Get-ADSIRandomUser is a function that retrieves a list of random active directory users using ADSI (Active Directory Service Inferfaces). The results are returned as objects for reusability. This function does not depend on the Active Directory PowerShell module. .PARAMETER Records The number of random Active Directory user records to retrieve. .EXAMPLE Get-ADSIRandomUser -Records 20 .INPUTS None .OUTPUTS System.Management.Automation.PSCustomObject #> [CmdletBinding()] param( [Parameter(Mandatory=$True)] [int]$Records ) try { $searcher=[adsisearcher]'(&(objectCategory=user)(objectClass=user))' $props=@( 'samaccountname', 'department', 'title', 'lastlogontimestamp', 'pwdlastset', 'useraccountcontrol', 'islockedout' ) $searcher.PropertiesToLoad.AddRange($props) $RandomUsers = $searcher.FindAll() | Get-Random -Count $Records } catch { Write-Warning -Message "Error retrieving active directory user information using ADSI" } foreach ($user in $RandomUsers) { [pscustomobject][ordered]@{ SamAccountName = $($user.Properties.samaccountname) Department = $($user.Properties.department) Title=$($user.Properties.title) LastLogonTimestamp=$($user.Properties.lastlogontimestamp) pwdLastSet=$($user.Properties.pwdlastset) Enabled = (-not($($user.GetDirectoryEntry().InvokeGet('AccountDisabled')))) LockedOut = $($user.GetDirectoryEntry().InvokeGet('IsAccountLocked')) } } } function Set-AlternatingCSS { <# .SYNOPSIS Setup an alternating cascading style sheet. .DESCRIPTION The Set-AlternatingCSS function is a modified version of a function from Don Jones's "Creating HTML Reports in PowerShell" book. Visit http://powershellbooks.com for details about this free ebook (Thank You Don!). .PARAMETER HTMLFragment The HTML fragment created with the ConvertTo-Html cmdlet that you wish to apply the alternating CSS to. .PARAMETER CSSEvenClass The CSS to apply to the even rows within the table. .PARAMETER CssOddClass The CSS to apply to the odd rows within the table. .EXAMPLE Set-AlternatingCSS -HTMLFragment ('My HTML' | ConvertTo-Html -Fragment | Out-String) -CSSEvenClass 'even' -CssOddClass 'odd' .EXAMPLE 'My HTML' | ConvertTo-Html -Fragment | Out-String |Set-AlternatingCSS -CSSEvenClass 'even' -CssOddClass 'odd' .INPUTS String .OUTPUTS String #> [CmdletBinding()] param( [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [string]$HTMLFragment, [Parameter(Mandatory=$True)] [string]$CSSEvenClass, [Parameter(Mandatory=$True)] [string]$CssOddClass ) [xml]$xml = $HTMLFragment $Table = $xml.SelectSingleNode('table') $Classname = $CSSOddClass foreach ($tr in $Table.tr) { if ($Classname -eq $CSSEvenClass) { $Classname = $CssOddClass } else { $Classname = $CSSEvenClass } $Class = $xml.CreateAttribute('class') $Class.value = $Classname $tr.attributes.append($Class) | Out-Null } $xml.innerxml | Out-String } |
This PowerShell script can also be downloaded from the TechNet script repository.
µ
Brilliant Greenbar Paper function! I used the CSS “:nth-child()” selector in my style sheet for my greenbar paper effect, but it didn’t work in IE10 for some reason. Reference: http://www.w3schools.com/cssref/sel_nth-child.asp