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.
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.
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}
µ