· 10 years ago · Jun 20, 2016, 09:30 AM
1#Requires -Version 3.0
2#This File is in Unicode format. Do not edit in an ASCII editor.
3
4<#
5.SYNOPSIS
6 Perform an Active Directory Health Check.
7.DESCRIPTION
8 Perform an Active Directory Health Check based on LDAP queries.
9 These are originally based on Jeff Wouters personal best practices.
10 No rights can be claimed by this report!
11
12 Founding guidelines for all checks in this script:
13 *) Must work for all domains in a forest tree.
14 *) Must work without module dependencies, except for the PowerShell core modules.
15 *) Must work without Administrator privileges.
16
17 You will see a lot of redirection to streams in this script. i.e. 3>$Null, 4>$Null and possibly *>$Null
18 This is explained here:
19 https://blogs.technet.microsoft.com/heyscriptingguy/2014/03/30/understanding-streams-redirection-and-write-host-in-powershell/
20.PARAMETER UserName
21 User name to use for the Cover Page and Footer.
22 Default value is contained in $env:username
23 This parameter has an alias of UN.
24.PARAMETER CompanyName
25 Companyname to use for the coverpage.
26 Default value is contained in HKCU:\Software\Microsoft\Office\Common\UserInfo\CompanyName
27 or HKCU:\Software\Microsoft\Office\Common\UserInfo\Company, whichever is populated on the
28 computer running the script.
29 This parameter has an alias of CN.
30.PARAMETER Coverpage
31 What Microsoft Word Cover Page to use.
32 Only Word 2010, 2013 and 2016 are supported.
33 (default cover pages in Word en-US)
34
35 Valid input is:
36 Alphabet (Word 2010. Works)
37 Annual (Word 2010. Doesn't work well for this report)
38 Austere (Word 2010. Works)
39 Austin (Word 2010/2013/2016. Doesn't work in 2013 or 2016, mostly works in 2010 but
40 Subtitle/Subject & Author fields need to be moved
41 after title box is moved up)
42 Banded (Word 2013/2016. Works)
43 Conservative (Word 2010. Works)
44 Contrast (Word 2010. Works)
45 Cubicles (Word 2010. Works)
46 Exposure (Word 2010. Works if you like looking sideways)
47 Facet (Word 2013/2016. Works)
48 Filigree (Word 2013/2016. Works)
49 Grid (Word 2010/2013/2016. Works in 2010)
50 Integral (Word 2013/2016. Works)
51 Ion (Dark) (Word 2013/2016. Top date doesn't fit; box needs to be manually resized or font
52 changed to 8 point)
53 Ion (Light) (Word 2013/2016. Top date doesn't fit; box needs to be manually resized or font
54 changed to 8 point)
55 Mod (Word 2010. Works)
56 Motion (Word 2010/2013/2016. Works if top date is manually changed to 36 point)
57 Newsprint (Word 2010. Works but date is not populated)
58 Perspective (Word 2010. Works)
59 Pinstripes (Word 2010. Works)
60 Puzzle (Word 2010. Top date doesn't fit; box needs to be manually resized or font
61 changed to 14 point)
62 Retrospect (Word 2013/2016. Works)
63 Semaphore (Word 2013/2016. Works)
64 Sideline (Word 2010/2013/2016. Doesn't work in 2013 or 2016, works in 2010)
65 Slice (Dark) (Word 2013/2016. Doesn't work)
66 Slice (Light) (Word 2013/2016. Doesn't work)
67 Stacks (Word 2010. Works)
68 Tiles (Word 2010. Date doesn't fit unless changed to 26 point)
69 Transcend (Word 2010. Works)
70 ViewMaster (Word 2013/2016. Works)
71 Whisp (Word 2013/2016. Works)
72
73 Default value is Sideline.
74 This parameter has an alias of CP.
75 This parameter is only valid with the MSWORD and PDF output parameters.
76.PARAMETER MSWord
77 SaveAs DOCX file.
78 This parameter is set True if no other output format is selected.
79.PARAMETER PDF
80 SaveAs PDF file instead of DOCX file.
81 This parameter is disabled by default.
82 The PDF file is roughly 5X to 10X larger than the DOCX file.
83.PARAMETER AddDateTime
84 Adds a date time stamp to the end of the file name.
85 Time stamp is in the format of yyyy-MM-dd_HHmm.
86 June 1, 2016 at 6PM is 2016-06-01_1800.
87 Output filename will be ReportName_2016-06-01_1800.docx (or .pdf).
88 This parameter is disabled by default.
89 This parameter has an alias of ADT.
90.PARAMETER Sites
91 Only perform the checks related to Sites.
92.PARAMETER OrganisationalUnit
93 Only perform the checks related to OrganisationalUnits.
94 This parameter has an alias of OU.
95.PARAMETER Users
96 Only perform the checks related to Users.
97.PARAMETER Computers
98 Only perform the checks related to Computers.
99.PARAMETER Groups
100 Only perform the checks related to Groups.
101.PARAMETER All
102 Perform all checks.
103 This parameter is the default if no other selection parameters are used.
104.PARAMETER Log
105 Generates a log file for the purpose of troubleshooting.
106.PARAMETER Mgmt
107 Provides a page at the end of the PDF or DOCX file with information for your manager.
108 Listed is the name of the check performed and the number of results found by the check.
109.PARAMETER Visible
110 Shows Microsoft Word while creating the report.
111 This parameter is disabled by default.
112.PARAMETER CSV
113 For each check, a separate CSV file will be created with the results.
114.PARAMETER Folder
115 Specifies the optional output folder to save the output report.
116.PARAMETER SmtpServer
117 Specifies the optional email server to send the output report.
118.PARAMETER SmtpPort
119 Specifies the SMTP port.
120 Default is 25.
121.PARAMETER UseSSL
122 Specifies whether to use SSL for the SmtpServer.
123 Default is False.
124.PARAMETER From
125 Specifies the username for the From email address.
126 If SmtpServer is used, this is a required parameter.
127.PARAMETER To
128 Specifies the username for the To email address.
129 If SmtpServer is used, this is a required parameter.
130.PARAMETER Dev
131 Clears errors at the beginning of the script.
132 Outputs all errors to a text file at the end of the script.
133
134 This is used when the script developer requests more troubleshooting data.
135 Text file is placed in the same folder from where the script is run.
136
137 This parameter is disabled by default.
138.PARAMETER ScriptInfo
139 Outputs information about the script to a text file.
140 Text file is placed in the same folder from where the script is run.
141
142 This parameter is disabled by default.
143 This parameter has an alias of SI.
144.EXAMPLE
145 PS C:\PSScript > .\ADHealthCheck_V2.ps1 -Visible -MSWord
146
147 This will generate a DOCX document with all the checks included.
148 Microsoft Word will be visible while creating the DOCX file.
149 The file is created at the location of the script that is executed.
150.EXAMPLE
151 PS C:\PSScript > .\ADHealthCheck_V2.ps1 -Visible -MSWord -Log -CSV
152
153 This will generate a DOCX document with all the checks included.
154 Microsoft Word will be visible while creating the DOCX file.
155 For each check, a separate CSV file will be created with the results.
156 A log file is created for the purpose of troubleshooting.
157 All files are created at the location of the script that is executed.
158.EXAMPLE
159 PS C:\PSScript > .\ADHealthCheck_V2.ps1 -MSWord -Sites -Users -Groups
160
161 This will generate a DOCX document with the checks for Sites, Users and Groups.
162.EXAMPLE
163 PS C:\PSScript > .\ADHealthCheck_V2.ps1 -Folder \\FileServer\ShareName
164
165 This will generate a DOCX document with all the checks included.
166 Output file will be saved in the path \\FileServer\ShareName
167.EXAMPLE
168 PS C:\PSScript > .\ADHealthCheck_V2.ps1 -SmtpServer mail.domain.tld -From ADAdmin@domain.tld -To ITGroup@domain.tld
169
170 Script will use the email server mail.domain.tld, sending from ADAdmin@domain.tld, sending to ITGroup@domain.tld.
171 If the current user's credentials are not valid to send email, the user will be prompted to enter valid credentials.
172.EXAMPLE
173 PS C:\PSScript > .\ADHealthCheck_V2.ps1 -SmtpServer smtp.office365.com -SmtpPort 587 -UseSSL -From Webster@CarlWebster.com -To ITGroup@CarlWebster.com
174
175 Script will use the email server smtp.office365.com on port 587 using SSL, sending from webster@carlwebster.com, sending to ITGroup@carlwebster.com.
176 If the current user's credentials are not valid to send email, the user will be prompted to enter valid credentials.
177.INPUTS
178 None. You cannot pipe objects to this script.
179.OUTPUTS
180 No objects are output from this script. This script creates a Word or PDF document.
181.NOTES
182 NAME : AD Health Check.ps1
183 AUTHOR : Jeff Wouters [MVP Windows PowerShell], Carl Webster and Michael B. Smith
184 VERSION : 2.0
185 LAST EDIT : 8-May-2016
186
187 The Word file generation part of the script is based upon the work done by:
188
189 Carl Webster | http://www.carlwebster.com | @CarlWebster
190 Iain Brighton | http://virtualengine.co.uk | @IainBrighton
191 Jeff Wouters | http://www.jeffwouters.nl | @JeffWouters
192
193 The Active Directory checks were originally written by:
194
195 Jeff Wouters | http://www.jeffwouters.nl | @JeffWouters
196
197 Significant Active Directory changes have been implemented by:
198
199 Michael B. Smith | http://TheEssentialExchange.com/ | @EssentialExchange
200#>
201
202[CmdletBinding( DefaultParameterSetName = 'All', SupportsShouldProcess = $false, ConfirmImpact = 'None' )]
203Param(
204 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
205 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
206 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
207 [Alias( 'UN' )]
208 [ValidateNotNullOrEmpty()]
209 [string] $UserName = $env:username,
210
211 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
212 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
213 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
214 [Alias( 'CN' )]
215 [ValidateNotNullOrEmpty()]
216 $CompanyName = '',
217
218 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
219 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
220 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
221 [Alias( 'CP' )]
222 [ValidateNotNullOrEmpty()]
223 [string] $CoverPage = 'Sideline',
224
225 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
226 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
227 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
228 [Switch] $MSWord = $false,
229
230 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
231 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
232 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
233 [Switch] $PDF = $false,
234
235 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
236 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
237 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
238 [Alias( 'ADT' )]
239 [Switch] $AddDateTime = $false,
240
241 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
242 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
243 [Switch] $Sites,
244
245 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
246 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
247 [Alias( 'OU' )]
248 [Alias( 'OrganizationalUnit' )]
249 [Switch] $OrganisationalUnit,
250
251 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
252 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
253 [Switch] $Users,
254
255 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
256 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
257 [Switch] $Computers,
258
259 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
260 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
261 [Switch] $Groups,
262
263 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
264 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
265 [Switch] $All = $true,
266
267 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
268 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
269 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
270 [Switch] $Log = $false,
271
272 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
273 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
274 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
275 [Alias( 'Management' )]
276 [Switch] $Mgmt = $false,
277
278 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
279 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
280 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
281 [Switch] $Visible = $false,
282
283 [Parameter( Mandatory = $false, ParameterSetName = 'Specific' )]
284 [Parameter( Mandatory = $false, ParameterSetName = 'All' )]
285 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
286 [Switch] $CSV = $false,
287
288 [Parameter( Mandatory = $false )]
289 [string] $Folder = '',
290
291 [Parameter( Mandatory = $true, ParameterSetName = 'SMTP' )]
292 [string] $SmtpServer = '',
293
294 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
295 [int]$SmtpPort = 25,
296
297 [Parameter( Mandatory = $false, ParameterSetName = 'SMTP' )]
298 [Switch] $UseSSL = $false,
299
300 [Parameter( Mandatory = $true, ParameterSetName = 'SMTP' )]
301 [string] $From = '',
302
303 [Parameter( Mandatory = $true, ParameterSetName = 'SMTP' )]
304 [string] $To = '',
305
306 [Parameter( Mandatory = $false )]
307 [Switch] $Dev = $false,
308
309 [Parameter( Mandatory = $false )]
310 [Alias( 'SI' )]
311 [Switch] $ScriptInfo = $false
312)
313
314#region script change log
315#originally written by Jeff Wouters | http://www.jeffwouters.nl | @JeffWouters
316# Now maintained by webster@carlwebster.com
317#@carlwebster on Twitter
318#http://www.CarlWebster.com
319#
320#Version 2.0 9-May-2016
321# Added alias for AddDateTime of ADT
322# Added alias for CompanyName of CN
323# Added -Dev parameter to create a text file of script errors
324# Added more script information to the console output when script starts
325# Added -ScriptInfo (SI) parameter to create a text file of script information
326# Added support for emailing output report
327# Added support for output folder
328# Added word 2016 support
329# Fixed numerous issues discovered with the latest update to PowerShell V5
330# Fixed several incorrect variable names that kept PDFs from saving in Windows 10 and Office 2013
331# General code cleanup by Michael B. Smith
332# Output to CSV rewritten by Michael B. Smith
333# Removed the 10 second pauses waiting for Word to save and close
334# Removed unused parameters Text, HTML, ComputerName, Hardware
335# Significant Active Directory changes have been implemented by Michael B. Smith
336# Updated help text
337#
338# Version 1.0 released to the community on July 14, 2014
339# http://jeffwouters.nl/index.php/2014/07/an-active-directory-health-check-powershell-script-v1-0/
340#endregion
341
342Set-StrictMode -Version 2
343
344#force -verbose on
345$PSDefaultParameterValues = @{"*:Verbose"=$True}
346$SaveEAPreference = $ErrorActionPreference
347$ErrorActionPreference = 'SilentlyContinue'
348##$Script:ThisScriptPath = $(Split-Path ((Get-PSCallStack)[0]).ScriptName) -- this is crap after v1
349$Script:ThisScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
350
351If($PSBoundParameters.ContainsKey('Log'))
352{
353 $Script:LogPath = "$Script:ThisScriptPath\ADHealthCheckTranscript.txt"
354 If((Test-Path $Script:LogPath) -eq $true)
355 {
356 Write-Verbose "$(Get-Date): Transcript/Log $Script:LogPath already exists"
357 $Script:StartLog = $false
358 }
359 Else
360 {
361 try
362 {
363 Start-Transcript -Path $Script:LogPath -Force -Verbose:$false | Out-Null
364 Write-Verbose "$(Get-Date): Transcript/log started at $Script:LogPath"
365 $Script:StartLog = $true
366 }
367 catch
368 {
369 Write-Verbose "$(Get-Date): Transcript/log failed at $Script:LogPath"
370 $Script:StartLog = $false
371 }
372 }
373}
374
375If($Null -eq $PDF)
376{
377 $PDF = $False
378}
379If($Null -eq $MSWord)
380{
381 $MSWord = $False
382}
383If($Null -eq $AddDateTime)
384{
385 $AddDateTime = $False
386}
387If($Null -eq $Folder)
388{
389 $Folder = ""
390}
391If($Null -eq $SmtpServer)
392{
393 $SmtpServer = ""
394}
395If($Null -eq $SmtpPort)
396{
397 $SmtpPort = 25
398}
399If($Null -eq $UseSSL)
400{
401 $UseSSL = $False
402}
403If($Null -eq $From)
404{
405 $From = ""
406}
407If($Null -eq $To)
408{
409 $To = ""
410}
411If($Null -eq $Dev)
412{
413 $Dev = $False
414}
415If($Null -eq $ScriptInfo)
416{
417 $ScriptInfo = $False
418}
419
420If(!(Test-Path Variable:PDF))
421{
422 $PDF = $False
423}
424If(!(Test-Path Variable:MSWord))
425{
426 $MSWord = $False
427}
428If(!(Test-Path Variable:AddDateTime))
429{
430 $AddDateTime = $False
431}
432If(!(Test-Path Variable:Folder))
433{
434 $Folder = ""
435}
436If(!(Test-Path Variable:SmtpServer))
437{
438 $SmtpServer = ""
439}
440If(!(Test-Path Variable:SmtpPort))
441{
442 $SmtpPort = 25
443}
444If(!(Test-Path Variable:UseSSL))
445{
446 $UseSSL = $False
447}
448If(!(Test-Path Variable:From))
449{
450 $From = ""
451}
452If(!(Test-Path Variable:To))
453{
454 $To = ""
455}
456If(!(Test-Path Variable:Dev))
457{
458 $Dev = $False
459}
460If(!(Test-Path Variable:ScriptInfo))
461{
462 $ScriptInfo = $False
463}
464
465If($Null -eq $MSWord)
466{
467 If($PDF)
468 {
469 $MSWord = $False
470 }
471 Else
472 {
473 $MSWord = $True
474 }
475}
476
477If($MSWord -eq $False -and $PDF -eq $False)
478{
479 $MSWord = $True
480}
481
482Write-Verbose "$(Get-Date): Testing output parameters"
483
484If($MSWord)
485{
486 Write-Verbose "$(Get-Date): MSWord is set"
487}
488ElseIf($PDF)
489{
490 Write-Verbose "$(Get-Date): PDF is set"
491}
492Else
493{
494 $ErrorActionPreference = $SaveEAPreference
495 Write-Verbose "$(Get-Date): Unable to determine output parameter"
496 If($Null -eq $MSWord)
497 {
498 Write-Verbose "$(Get-Date): MSWord is Null"
499 }
500 ElseIf($Null -eq $PDF)
501 {
502 Write-Verbose "$(Get-Date): PDF is Null"
503 }
504 Else
505 {
506 Write-Verbose "$(Get-Date): MSWord is " $MSWord
507 Write-Verbose "$(Get-Date): PDF is " $PDF
508 }
509 Write-Error "Unable to determine output parameter. Script cannot continue"
510 Exit
511}
512
513If($Folder -ne "")
514{
515 Write-Verbose "$(Get-Date): Testing folder path"
516 #does it exist
517 If(Test-Path $Folder -EA 0)
518 {
519 #it exists, now check to see if it is a folder and not a file
520 If(Test-Path $Folder -pathType Container -EA 0)
521 {
522 #it exists and it is a folder
523 Write-Verbose "$(Get-Date): Folder path $Folder exists and is a folder"
524 }
525 Else
526 {
527 #it exists but it is a file not a folder
528 Write-Error "Folder $Folder is a file, not a folder. Script cannot continue"
529 $ErrorActionPreference = $SaveEAPreference
530 Exit
531 }
532 }
533 Else
534 {
535 #does not exist
536 Write-Error "Folder $Folder does not exist. Script cannot continue"
537 $ErrorActionPreference = $SaveEAPreference
538 Exit
539 }
540}
541
542If($Dev)
543{
544 $Error.Clear()
545 $pwdPath = $Folder
546 If($pwdPath -eq "")
547 {
548 $pwdpath = $pwd.Path
549 }
550
551 [string] $Script:DevErrorFile = Join-Path $pwdPath "ADHealthCheckScriptErrors_$(Get-Date -f yyyy-MM-dd_HHmm).txt"
552}
553
554If($ScriptInfo)
555{
556 $pwdPath = $Folder
557 If($pwdPath -eq "")
558 {
559 $pwdpath = $pwd.Path
560 }
561
562 [string] $Script:SIFile = Join-Path $pwdPath "ADHealthCheckScriptInfo_$(Get-Date -f yyyy-MM-dd_HHmm).txt"
563}
564
565[string]$Script:RunningOS = (Get-WmiObject -class Win32_OperatingSystem -EA 0).Caption
566
567If($MSWord -or $PDF)
568{
569 #try and fix the issue with the $CompanyName variable
570 $Script:CoName = $CompanyName
571 Write-Verbose "$(Get-Date): CoName is $($Script:CoName)"
572
573 #the following values were attained from
574 #http://groovy.codehaus.org/modules/scriptom/1.6.0/scriptom-office-2K3-tlb/apidocs/
575 #http://msdn.microsoft.com/en-us/library/office/aa211923(v=office.11).aspx
576 [int]$wdAlignPageNumberRight = 2
577 [long]$wdColorGray15 = 14277081
578 [long]$wdColorGray05 = 15987699
579 [int]$wdMove = 0
580 [int]$wdSeekMainDocument = 0
581 [int]$wdSeekPrimaryFooter = 4
582 [int]$wdStory = 6
583 [long]$wdColorRed = 255
584 [int]$wdColorBlack = 0
585 [int]$wdWord2007 = 12
586 [int]$wdWord2010 = 14
587 [int]$wdWord2013 = 15
588 [int]$wdWord2016 = 16
589 [int]$wdFormatDocumentDefault = 16
590 [int]$wdFormatPDF = 17
591 #http://blogs.technet.com/b/heyscriptingguy/archive/2006/03/01/how-can-i-right-align-a-single-column-in-a-word-table.aspx
592 #http://msdn.microsoft.com/en-us/library/office/ff835817%28v=office.15%29.aspx
593 [int]$wdAlignParagraphLeft = 0
594 [int]$wdAlignParagraphCenter = 1
595 [int]$wdAlignParagraphRight = 2
596 #http://msdn.microsoft.com/en-us/library/office/ff193345%28v=office.15%29.aspx
597 [int]$wdCellAlignVerticalTop = 0
598 [int]$wdCellAlignVerticalCenter = 1
599 [int]$wdCellAlignVerticalBottom = 2
600 #http://msdn.microsoft.com/en-us/library/office/ff844856%28v=office.15%29.aspx
601 [int]$wdAutoFitFixed = 0
602 [int]$wdAutoFitContent = 1
603 [int]$wdAutoFitWindow = 2
604 #http://msdn.microsoft.com/en-us/library/office/ff821928%28v=office.15%29.aspx
605 [int]$wdAdjustNone = 0
606 [int]$wdAdjustProportional = 1
607 [int]$wdAdjustFirstColumn = 2
608 [int]$wdAdjustSameWidth = 3
609
610 [int]$PointsPerTabStop = 36
611 [int]$Indent0TabStops = 0 * $PointsPerTabStop
612 [int]$Indent1TabStops = 1 * $PointsPerTabStop
613 [int]$Indent2TabStops = 2 * $PointsPerTabStop
614 [int]$Indent3TabStops = 3 * $PointsPerTabStop
615 [int]$Indent4TabStops = 4 * $PointsPerTabStop
616
617 # http://www.thedoctools.com/index.php?show=wt_style_names_english_danish_german_french
618 [int]$wdStyleHeading1 = -2
619 [int]$wdStyleHeading2 = -3
620 [int]$wdStyleHeading3 = -4
621 [int]$wdStyleHeading4 = -5
622 [int]$wdStyleNoSpacing = -158
623 [int]$wdTableGrid = -155
624
625 #http://groovy.codehaus.org/modules/scriptom/1.6.0/scriptom-office-2K3-tlb/apidocs/org/codehaus/groovy/scriptom/tlb/office/word/WdLineStyle.html
626 [int]$wdLineStyleNone = 0
627 [int]$wdLineStyleSingle = 1
628
629 [int]$wdHeadingFormatTrue = -1
630 [int]$wdHeadingFormatFalse = 0
631}
632
633Function SetWordHashTable
634{
635 Param(
636 [string]$CultureCode
637 )
638
639 $hash = @{}
640
641 # DE and FR translations for Word 2010 by Vladimir Radojevic
642 # Vladimir.Radojevic@Commerzreal.com
643
644 # DA translations for Word 2010 by Thomas Daugaard
645 # Citrix Infrastructure Specialist at edgemo A/S
646
647 # CA translations by Javier Sanchez
648 # CEO & Founder 101 Consulting
649
650 #ca - Catalan
651 #da - Danish
652 #de - German
653 #en - English
654 #es - Spanish
655 #fi - Finnish
656 #fr - French
657 #nb - Norwegian
658 #nl - Dutch
659 #pt - Portuguese
660 #sv - Swedish
661
662 Switch ($CultureCode)
663 {
664 'ca-' {
665 $hash.($($CultureCode)) = @{
666 'Word_TableOfContents' = 'Taula automática 2'
667 }
668 }
669
670 'da-' {
671 $hash.($($CultureCode)) = @{
672 'Word_TableOfContents' = 'Automatisk tabel 2'
673 }
674 }
675
676 'de-' {
677 $hash.($($CultureCode)) = @{
678 'Word_TableOfContents' = 'Automatische Tabelle 2'
679 }
680 }
681
682 'en-' {
683 $hash.($($CultureCode)) = @{
684 'Word_TableOfContents' = 'Automatic Table 2'
685 }
686 }
687
688 'es-' {
689 $hash.($($CultureCode)) = @{
690 'Word_TableOfContents' = 'Tabla automática 2'
691 }
692 }
693
694 'fi-' {
695 $hash.($($CultureCode)) = @{
696 'Word_TableOfContents' = 'Automaattinen taulukko 2'
697 }
698 }
699
700 'fr-' {
701 $hash.($($CultureCode)) = @{
702 'Word_TableOfContents' = 'Sommaire Automatique 2'
703 }
704 }
705
706 'nb-' {
707 $hash.($($CultureCode)) = @{
708 'Word_TableOfContents' = 'Automatisk tabell 2'
709 }
710 }
711
712 'nl-' {
713 $hash.($($CultureCode)) = @{
714 'Word_TableOfContents' = 'Automatische inhoudsopgave 2'
715 }
716 }
717
718 'pt-' {
719 $hash.($($CultureCode)) = @{
720 'Word_TableOfContents' = 'Sumário Automático 2'
721 }
722 }
723
724 'sv-' {
725 $hash.($($CultureCode)) = @{
726 'Word_TableOfContents' = 'Automatisk innehållsförteckning2'
727 }
728 }
729
730 Default {$hash.('en-') = @{
731 'Word_TableOfContents' = 'Automatic Table 2'
732 }
733 }
734 }
735
736 $Script:myHash = $hash.$CultureCode
737
738 If($Script:myHash -eq $Null)
739 {
740 $Script:myHash = $hash.('en-')
741 }
742
743 $Script:myHash.Word_NoSpacing = $wdStyleNoSpacing
744 $Script:myHash.Word_Heading1 = $wdStyleheading1
745 $Script:myHash.Word_Heading2 = $wdStyleheading2
746 $Script:myHash.Word_Heading3 = $wdStyleheading3
747 $Script:myHash.Word_Heading4 = $wdStyleheading4
748 $Script:myHash.Word_TableGrid = $wdTableGrid
749}
750
751Function GetCulture
752{
753 Param(
754 [int]$WordValue
755 )
756
757 #codes obtained from http://support.microsoft.com/kb/221435
758 #http://msdn.microsoft.com/en-us/library/bb213877(v=office.12).aspx
759 $CatalanArray = ,1027
760 $DanishArray = ,1030
761 $DutchArray = 2067, 1043
762 $EnglishArray = 3081, 10249, 4105, 9225, 6153, 8201, 5129, 13321, 7177, 11273, 2057, 1033, 12297
763 $FinnishArray = ,1035
764 $FrenchArray = 2060, 1036, 11276, 3084, 12300, 5132, 13324, 6156, 8204, 10252, 7180, 9228, 4108
765 $GermanArray = 1031, 3079, 5127, 4103, 2055
766 $NorwegianArray = 1044, 2068
767 $PortugueseArray = 1046, 2070
768 $SpanishArray = 1034, 11274, 16394, 13322, 9226, 5130, 7178, 12298, 17418, 4106, 18442, 19466, 6154, 15370, 10250, 20490, 3082, 14346, 8202
769 $SwedishArray = 1053, 2077
770
771 #ca - Catalan
772 #da - Danish
773 #de - German
774 #en - English
775 #es - Spanish
776 #fi - Finnish
777 #fr - French
778 #nb - Norwegian
779 #nl - Dutch
780 #pt - Portuguese
781 #sv - Swedish
782
783 Switch ($WordValue)
784 {
785 { $CatalanArray -contains $_ } { $CultureCode = "ca-" }
786 { $DanishArray -contains $_ } { $CultureCode = "da-" }
787 { $DutchArray -contains $_ } { $CultureCode = "nl-" }
788 { $EnglishArray -contains $_ } { $CultureCode = "en-" }
789 { $FinnishArray -contains $_ } { $CultureCode = "fi-" }
790 { $FrenchArray -contains $_ } { $CultureCode = "fr-" }
791 { $GermanArray -contains $_ } { $CultureCode = "de-" }
792 { $NorwegianArray -contains $_ } { $CultureCode = "nb-" }
793 { $PortugueseArray -contains $_ } { $CultureCode = "pt-" }
794 { $SpanishArray -contains $_ } { $CultureCode = "es-" }
795 { $SwedishArray -contains $_ } { $CultureCode = "sv-" }
796 Default { $CultureCode = "en-" }
797 }
798
799 Return $CultureCode
800}
801
802Function ValidateCoverPage
803{
804 Param(
805 [int]$xWordVersion,
806 [string]$xCP,
807 [string]$CultureCode
808 )
809
810 $xArray = ""
811
812 Switch ($CultureCode)
813 {
814 'ca-' {
815 If($xWordVersion -eq $wdWord2016)
816 {
817 $xArray = ("Austin", "En bandes", "Faceta", "Filigrana",
818 "Integral", "Ió (clar)", "Ió (fosc)", "LÃnia lateral",
819 "Moviment", "QuadrÃcula", "Retrospectiu", "Sector (clar)",
820 "Sector (fosc)", "Semà for", "Visualització principal", "Whisp")
821 }
822 ElseIf($xWordVersion -eq $wdWord2013)
823 {
824 $xArray = ("Austin", "En bandes", "Faceta", "Filigrana",
825 "Integral", "Ió (clar)", "Ió (fosc)", "LÃnia lateral",
826 "Moviment", "QuadrÃcula", "Retrospectiu", "Sector (clar)",
827 "Sector (fosc)", "Semà for", "Visualització", "Whisp")
828 }
829 ElseIf($xWordVersion -eq $wdWord2010)
830 {
831 $xArray = ("Alfabet", "Anual", "Austin", "Conservador",
832 "Contrast", "Cubicles", "Diplomà tic", "Exposició",
833 "LÃnia lateral", "Mod", "Mosiac", "Moviment", "Paper de diari",
834 "Perspectiva", "Piles", "QuadrÃcula", "Sobri",
835 "Transcendir", "Trencaclosques")
836 }
837 }
838
839 'da-' {
840 If($xWordVersion -eq $wdWord2016)
841 {
842 $xArray = ("Austin", "BevægElse", "Brusen", "Facet", "Filigran",
843 "Gitter", "Integral", "Ion (lys)", "Ion (mørk)",
844 "Retro", "Semafor", "Sidelinje", "Stribet",
845 "Udsnit (lys)", "Udsnit (mørk)", "Visningsmaster")
846 }
847 ElseIf($xWordVersion -eq $wdWord2013)
848 {
849 $xArray = ("BevægElse", "Brusen", "Ion (lys)", "Filigran",
850 "Retro", "Semafor", "Visningsmaster", "Integral",
851 "Facet", "Gitter", "Stribet", "Sidelinje", "Udsnit (lys)",
852 "Udsnit (mørk)", "Ion (mørk)", "Austin")
853 }
854 ElseIf($xWordVersion -eq $wdWord2010)
855 {
856 $xArray = ("BevægElse", "Moderat", "Perspektiv", "Firkanter",
857 "Overskrid", "Alfabet", "Kontrast", "Stakke", "Fliser", "GÃ¥de",
858 "Gitter", "Austin", "Eksponering", "Sidelinje", "Enkel",
859 "NÃ¥lestribet", "Ã…rlig", "Avispapir", "Tradionel")
860 }
861 }
862
863 'de-' {
864 If($xWordVersion -eq $wdWord2016)
865 {
866 $xArray = ("Austin", "Bewegung", "Facette", "Filigran",
867 "Gebändert", "Integral", "Ion (dunkel)", "Ion (hell)",
868 "Pfiff", "Randlinie", "Raster", "Rückblick",
869 "Segment (dunkel)", "Segment (hell)", "Semaphor",
870 "ViewMaster")
871 }
872 ElseIf($xWordVersion -eq $wdWord2013)
873 {
874 $xArray = ("Semaphor", "Segment (hell)", "Ion (hell)",
875 "Raster", "Ion (dunkel)", "Filigran", "Rückblick", "Pfiff",
876 "ViewMaster", "Segment (dunkel)", "Verbunden", "Bewegung",
877 "Randlinie", "Austin", "Integral", "Facette")
878 }
879 ElseIf($xWordVersion -eq $wdWord2010)
880 {
881 $xArray = ("Alphabet", "Austin", "Bewegung", "Durchscheinend",
882 "Herausgestellt", "Jährlich", "Kacheln", "Kontrast", "Kubistisch",
883 "Modern", "Nadelstreifen", "Perspektive", "Puzzle", "Randlinie",
884 "Raster", "Schlicht", "Stapel", "Traditionell", "Zeitungspapier")
885 }
886 }
887
888 'en-' {
889 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
890 {
891 $xArray = ("Austin", "Banded", "Facet", "Filigree", "Grid",
892 "Integral", "Ion (Dark)", "Ion (Light)", "Motion", "Retrospect",
893 "Semaphore", "Sideline", "Slice (Dark)", "Slice (Light)", "ViewMaster",
894 "Whisp")
895 }
896 ElseIf($xWordVersion -eq $wdWord2010)
897 {
898 $xArray = ("Alphabet", "Annual", "Austere", "Austin", "Conservative",
899 "Contrast", "Cubicles", "Exposure", "Grid", "Mod", "Motion", "Newsprint",
900 "Perspective", "Pinstripes", "Puzzle", "Sideline", "Stacks", "Tiles", "Transcend")
901 }
902 }
903
904 'es-' {
905 If($xWordVersion -eq $wdWord2016)
906 {
907 $xArray = ("Austin", "Con bandas", "Cortar (oscuro)", "CuadrÃcula",
908 "Whisp", "Faceta", "Filigrana", "Integral", "Ion (claro)",
909 "Ion (oscuro)", "LÃnea lateral", "Movimiento", "Retrospectiva",
910 "Semáforo", "Slice (luz)", "Vista principal", "Whisp")
911 }
912 ElseIf($xWordVersion -eq $wdWord2013)
913 {
914 $xArray = ("Whisp", "Vista principal", "Filigrana", "Austin",
915 "Slice (luz)", "Faceta", "Semáforo", "Retrospectiva", "CuadrÃcula",
916 "Movimiento", "Cortar (oscuro)", "LÃnea lateral", "Ion (oscuro)",
917 "Ion (claro)", "Integral", "Con bandas")
918 }
919 ElseIf($xWordVersion -eq $wdWord2010)
920 {
921 $xArray = ("Alfabeto", "Anual", "Austero", "Austin", "Conservador",
922 "Contraste", "CuadrÃcula", "CubÃculos", "Exposición", "LÃnea lateral",
923 "Moderno", "Mosaicos", "Movimiento", "Papel periódico",
924 "Perspectiva", "Pilas", "Puzzle", "Rayas", "Sobrepasar")
925 }
926 }
927
928 'fi-' {
929 If($xWordVersion -eq $wdWord2016)
930 {
931 $xArray = ("Filigraani", "Integraali", "Ioni (tumma)",
932 "Ioni (vaalea)", "Opastin", "Pinta", "Retro", "Sektori (tumma)",
933 "Sektori (vaalea)", "Vaihtuvavärinen", "ViewMaster", "Austin",
934 "Kuiskaus", "Liike", "Ruudukko", "Sivussa")
935 }
936 ElseIf($xWordVersion -eq $wdWord2013)
937 {
938 $xArray = ("Filigraani", "Integraali", "Ioni (tumma)",
939 "Ioni (vaalea)", "Opastin", "Pinta", "Retro", "Sektori (tumma)",
940 "Sektori (vaalea)", "Vaihtuvavärinen", "ViewMaster", "Austin",
941 "Kiehkura", "Liike", "Ruudukko", "Sivussa")
942 }
943 ElseIf($xWordVersion -eq $wdWord2010)
944 {
945 $xArray = ("Aakkoset", "Askeettinen", "Austin", "Kontrasti",
946 "Laatikot", "Liike", "Liituraita", "Mod", "Osittain peitossa",
947 "Palapeli", "Perinteinen", "Perspektiivi", "Pinot", "Ruudukko",
948 "Ruudut", "Sanomalehtipaperi", "Sivussa", "Vuotuinen", "Ylitys")
949 }
950 }
951
952 'fr-' {
953 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
954 {
955 $xArray = ("À bandes", "Austin", "Facette", "Filigrane",
956 "Guide", "Intégrale", "Ion (clair)", "Ion (foncé)",
957 "Lignes latérales", "Quadrillage", "Rétrospective", "Secteur (clair)",
958 "Secteur (foncé)", "Sémaphore", "ViewMaster", "Whisp")
959 }
960 ElseIf($xWordVersion -eq $wdWord2010)
961 {
962 $xArray = ("Alphabet", "Annuel", "Austère", "Austin",
963 "Blocs empilés", "Classique", "Contraste", "Emplacements de bureau",
964 "Exposition", "Guide", "Ligne latérale", "Moderne",
965 "Mosaïques", "Mots croisés", "Papier journal", "Perspective",
966 "Quadrillage", "Rayures fines", "Transcendant")
967 }
968 }
969
970 'nb-' {
971 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
972 {
973 $xArray = ("Austin", "BevegElse", "Dempet", "Fasett", "Filigran",
974 "Integral", "Ion (lys)", "Ion (mørk)", "Retrospekt", "Rutenett",
975 "Sektor (lys)", "Sektor (mørk)", "Semafor", "Sidelinje", "Stripet",
976 "ViewMaster")
977 }
978 ElseIf($xWordVersion -eq $wdWord2010)
979 {
980 $xArray = ("Alfabet", "Ã…rlig", "Avistrykk", "Austin", "Avlukker",
981 "BevegElse", "Engasjement", "Enkel", "Fliser", "Konservativ",
982 "Kontrast", "Mod", "Perspektiv", "Puslespill", "Rutenett", "Sidelinje",
983 "Smale striper", "Stabler", "Transcenderende")
984 }
985 }
986
987 'nl-' {
988 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
989 {
990 $xArray = ("Austin", "Beweging", "Facet", "Filigraan", "Gestreept",
991 "Integraal", "Ion (donker)", "Ion (licht)", "Raster",
992 "Segment (Light)", "Semafoor", "Slice (donker)", "Spriet",
993 "Terugblik", "Terzijde", "ViewMaster")
994 }
995 ElseIf($xWordVersion -eq $wdWord2010)
996 {
997 $xArray = ("Aantrekkelijk", "Alfabet", "Austin", "Bescheiden",
998 "Beweging", "Blikvanger", "Contrast", "Eenvoudig", "Jaarlijks",
999 "Krantenpapier", "Krijtstreep", "Kubussen", "Mod", "Perspectief",
1000 "Puzzel", "Raster", "Stapels",
1001 "Tegels", "Terzijde")
1002 }
1003 }
1004
1005 'pt-' {
1006 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
1007 {
1008 $xArray = ("Animação", "Austin", "Em Tiras", "Exibição Mestra",
1009 "Faceta", "Fatia (Clara)", "Fatia (Escura)", "Filete", "Filigrana",
1010 "Grade", "Integral", "Ãon (Claro)", "Ãon (Escuro)", "Linha Lateral",
1011 "Retrospectiva", "Semáforo")
1012 }
1013 ElseIf($xWordVersion -eq $wdWord2010)
1014 {
1015 $xArray = ("Alfabeto", "Animação", "Anual", "Austero", "Austin", "Baias",
1016 "Conservador", "Contraste", "Exposição", "Grade", "Ladrilhos",
1017 "Linha Lateral", "Listras", "Mod", "Papel Jornal", "Perspectiva", "Pilhas",
1018 "Quebra-cabeça", "Transcend")
1019 }
1020 }
1021
1022 'sv-' {
1023 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
1024 {
1025 $xArray = ("Austin", "Band", "Fasett", "Filigran", "Integrerad", "Jon (ljust)",
1026 "Jon (mörkt)", "Knippe", "Rutnät", "RörElse", "Sektor (ljus)", "Sektor (mörk)",
1027 "Semafor", "Sidlinje", "VisaHuvudsida", "Ã…terblick")
1028 }
1029 ElseIf($xWordVersion -eq $wdWord2010)
1030 {
1031 $xArray = ("Alfabetmönster", "Austin", "Enkelt", "Exponering", "Konservativt",
1032 "Kontrast", "Kritstreck", "Kuber", "Perspektiv", "Plattor", "Pussel", "Rutnät",
1033 "RörElse", "Sidlinje", "Sobert", "Staplat", "Tidningspapper", "Årligt",
1034 "Övergående")
1035 }
1036 }
1037
1038 Default {
1039 If($xWordVersion -eq $wdWord2013 -or $xWordVersion -eq $wdWord2016)
1040 {
1041 $xArray = ("Austin", "Banded", "Facet", "Filigree", "Grid",
1042 "Integral", "Ion (Dark)", "Ion (Light)", "Motion", "Retrospect",
1043 "Semaphore", "Sideline", "Slice (Dark)", "Slice (Light)", "ViewMaster",
1044 "Whisp")
1045 }
1046 ElseIf($xWordVersion -eq $wdWord2010)
1047 {
1048 $xArray = ("Alphabet", "Annual", "Austere", "Austin", "Conservative",
1049 "Contrast", "Cubicles", "Exposure", "Grid", "Mod", "Motion", "Newsprint",
1050 "Perspective", "Pinstripes", "Puzzle", "Sideline", "Stacks", "Tiles", "Transcend")
1051 }
1052 }
1053 }
1054
1055 If($xArray -contains $xCP)
1056 {
1057 $xArray = $Null
1058 Return $True
1059 }
1060 Else
1061 {
1062 $xArray = $Null
1063 Return $False
1064 }
1065}
1066
1067Function Stop-WinWord
1068{
1069 Write-Debug "***Enter Stop-WinWord"
1070
1071 ## determine our login session
1072 $proc = Get-Process -PID $PID
1073 If( $null -eq $proc )
1074 {
1075 throw "Stop-WinWord: Cannot find process $PID"
1076 }
1077
1078 $SessionID = $proc.SessionId
1079 If( $null -eq $SessionID )
1080 {
1081 Write-Debug "Stop-WinWord: SessionId on $PID is null"
1082 throw "Can't find a session for pid $PID"
1083 }
1084
1085 If( 0 -eq $SessionID )
1086 {
1087 Write-Debug "Stop-WinWord: SessionId is 0 -- that is a bug"
1088 throw "SessionId is zero for pid $PID"
1089 }
1090
1091 #Find out if winword is running in our session
1092 try
1093 {
1094 $wordProc = Get-Process 'WinWord' -ErrorAction SilentlyContinue
1095 }
1096 catch
1097 {
1098 Write-Debug "***Exit Stop-WinWord: no WinWord tasks are running #1"
1099 Return ## not running
1100 }
1101
1102 If( !$wordproc )
1103 {
1104 Write-Debug "***Exit Stop-WinWord: no WinWord tasks are running #2"
1105 Return ## WinWord is not running in ANY session
1106 }
1107
1108 $wordrunning = $wordProc |? { $_.SessionId -eq $SessionID }
1109 If( !$wordrunning )
1110 {
1111 Write-Debug "***Exit Stop-WinWord: wordRunning eq null"
1112 Return ## not running in the current session
1113 }
1114 If( $wordrunning -is [Array] )
1115 {
1116 Write-Debug "***Exit Stop-WinWord: wordRunning is an array, elements=$($wordrunning.Count)"
1117 throw "Multiple Word processes are running in session $SessionID"
1118 }
1119
1120 ## it is possible for the below to throw a fault if Winword stops before it is executed.
1121 Stop-Process -Id $wordrunning.Id -ErrorAction SilentlyContinue
1122 Write-Debug "***Exit Stop-WinWord: sent Stop-Process to $($wordrunning.Id)"
1123}
1124
1125Function CheckWordPrereq
1126{
1127 If((Test-Path REGISTRY::HKEY_CLASSES_ROOT\Word.Application) -eq $False)
1128 {
1129 $ErrorActionPreference = $SaveEAPreference
1130 Write-Host "`n`n`t`tThis script directly outputs to Microsoft Word, please install Microsoft Word`n`n"
1131 Exit
1132 }
1133
1134 # If Word is running - then stop it
1135 Stop-WinWord
1136}
1137
1138Function ValidateCompanyName
1139{
1140 [bool]$xResult = Test-RegistryValue "HKCU:\Software\Microsoft\Office\Common\UserInfo" "CompanyName"
1141 If($xResult)
1142 {
1143 Return Get-RegistryValue "HKCU:\Software\Microsoft\Office\Common\UserInfo" "CompanyName"
1144 }
1145 Else
1146 {
1147 $xResult = Test-RegistryValue "HKCU:\Software\Microsoft\Office\Common\UserInfo" "Company"
1148 If($xResult)
1149 {
1150 Return Get-RegistryValue "HKCU:\Software\Microsoft\Office\Common\UserInfo" "Company"
1151 }
1152 Else
1153 {
1154 Return ""
1155 }
1156 }
1157}
1158
1159#http://stackoverflow.com/questions/5648931/test-if-registry-value-exists
1160# This Function just gets $True or $False
1161Function Test-RegistryValue($path, $name)
1162{
1163 $key = Get-Item -LiteralPath $path -EA 0
1164 $key -and $Null -ne $key.GetValue($name, $Null)
1165}
1166
1167# Gets the specified registry value or $Null if it is missing
1168Function Get-RegistryValue($path, $name)
1169{
1170 $key = Get-Item -LiteralPath $path -EA 0
1171 If($key)
1172 {
1173 $key.GetValue($name, $Null)
1174 }
1175 Else
1176 {
1177 $Null
1178 }
1179}
1180
1181Function WriteWordLine
1182#Function created by Ryan Revord
1183#@rsrevord on Twitter
1184#Function created to make output to Word easy in this script
1185#updated 27-Mar-2014 to include font name, font size, italics and bold options
1186#update 5-May-2016 by Michael B. Smith
1187{
1188 Param(
1189 [int] $style = 0,
1190 [int] $tabs = 0,
1191 [string] $name = '',
1192 [string] $value = '',
1193 [string] $fontName = $null,
1194 [int] $fontSize = 0,
1195 [bool] $italics = $false,
1196 [bool] $boldface = $false,
1197 [Switch] $nonewline
1198 )
1199
1200 #Build output style
1201 [string]$output = ''
1202 Switch ($style)
1203 {
1204 0 {$Script:Selection.Style = $myHash.Word_NoSpacing}
1205 1 {$Script:Selection.Style = $myHash.Word_Heading1}
1206 2 {$Script:Selection.Style = $myHash.Word_Heading2}
1207 3 {$Script:Selection.Style = $myHash.Word_Heading3}
1208 4 {$Script:Selection.Style = $myHash.Word_Heading4}
1209 Default {$Script:Selection.Style = $myHash.Word_NoSpacing}
1210 }
1211
1212 #build # of tabs
1213 While($tabs -gt 0)
1214 {
1215 $output += "`t"
1216 $tabs--
1217 }
1218
1219 If(![String]::IsNullOrEmpty($fontName))
1220 {
1221 $Script:Selection.Font.name = $fontName
1222 }
1223
1224 If($fontSize -ne 0)
1225 {
1226 $Script:Selection.Font.size = $fontSize
1227 }
1228
1229 If($italics -eq $True)
1230 {
1231 $Script:Selection.Font.Italic = $True
1232 }
1233
1234 If($boldface -eq $True)
1235 {
1236 $Script:Selection.Font.Bold = $True
1237 }
1238
1239 #output the rest of the parameters.
1240 $output += $name + $value
1241 $Script:Selection.TypeText($output)
1242
1243 #test for new WriteWordLine 0.
1244 If( !$nonewline )
1245 {
1246 $Script:Selection.TypeParagraph()
1247 }
1248}
1249Function _SetDocumentProperty
1250{
1251 #jeff hicks
1252 Param(
1253 [object] $Properties,
1254 [string] $Name,
1255 [string] $Value
1256 )
1257 #get the property object
1258 $prop = $properties | ForEach {
1259 $propname=$_.GetType().InvokeMember("Name","GetProperty",$Null,$_,$Null)
1260 If($propname -eq $Name)
1261 {
1262 Return $_
1263 }
1264 } #ForEach
1265
1266 #set the value
1267 $Prop.GetType().InvokeMember("Value","SetProperty",$Null,$prop,$Value)
1268}
1269
1270Function AbortScript
1271{
1272 $Word.Quit()
1273 Write-Verbose "$(Get-Date): System Cleanup"
1274 [System.Runtime.Interopservices.Marshal]::ReleaseComObject( $Word ) | Out-Null
1275 If( Get-Variable -Name Word -Scope Global )
1276 {
1277 Remove-Variable -Name word -Scope Global
1278 }
1279 [GC]::Collect()
1280 [GC]::WaitForPendingFinalizers()
1281 Write-Verbose "$(Get-Date): Script has been aborted"
1282 $ErrorActionPreference = $SaveEAPreference
1283 Exit
1284}
1285
1286Function FindWordDocumentEnd
1287{
1288 #Return focus to main document
1289 $Script:Doc.ActiveWindow.ActivePane.view.SeekView = $wdSeekMainDocument
1290 #move to the end of the current document
1291 $Script:Selection.EndKey($wdStory,$wdMove) | Out-Null
1292}
1293
1294<#
1295.Synopsis
1296 Add a table to a Microsoft Word document
1297.DESCRIPTION
1298 This Function adds a table to a Microsoft Word document from either an array of
1299 Hashtables or an array of PSCustomObjects.
1300
1301 Using this Function is quicker than setting each table cell individually but can
1302 only utilise the built-in MS Word table autoformats. Individual tables cells can
1303 be altered after the table has been appended to the document (a table reference
1304 is Returned).
1305.EXAMPLE
1306 AddWordTable -Hashtable $HashtableArray
1307
1308 This example adds table to the MS Word document, utilising all key/value pairs in
1309 the array of hashtables. Column headers will display the key names as defined.
1310 Note: the columns might not be displayed in the order that they were defined. To
1311 ensure columns are displayed in the required order utilise the -Columns parameter.
1312.EXAMPLE
1313 AddWordTable -Hashtable $HashtableArray -List
1314
1315 This example adds table to the MS Word document, utilising all key/value pairs in
1316 the array of hashtables. No column headers will be added, in a ListView format.
1317 Note: the columns might not be displayed in the order that they were defined. To
1318 ensure columns are displayed in the required order utilise the -Columns parameter.
1319.EXAMPLE
1320 AddWordTable -CustomObject $PSCustomObjectArray
1321
1322 This example adds table to the MS Word document, utilising all note property names
1323 the array of PSCustomObjects. Column headers will display the note property names.
1324 Note: the columns might not be displayed in the order that they were defined. To
1325 ensure columns are displayed in the required order utilise the -Columns parameter.
1326.EXAMPLE
1327 AddWordTable -Hashtable $HashtableArray -Columns FirstName,LastName,EmailAddress
1328
1329 This example adds a table to the MS Word document, but only using the specified
1330 key names: FirstName, LastName and EmailAddress. If other keys are present in the
1331 array of Hashtables they will be ignored.
1332.EXAMPLE
1333 AddWordTable -CustomObject $PSCustomObjectArray -Columns FirstName,LastName,EmailAddress -Headers "First Name","Last Name","Email Address"
1334
1335 This example adds a table to the MS Word document, but only using the specified
1336 PSCustomObject note properties: FirstName, LastName and EmailAddress. If other note
1337 properties are present in the array of PSCustomObjects they will be ignored. The
1338 display names for each specified column header has been overridden to display a
1339 custom header. Note: the order of the header names must match the specified columns.
1340#>
1341Function AddWordTable
1342{
1343 [CmdletBinding()]
1344 Param
1345 (
1346 # Array of Hashtable (including table headers)
1347 [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName='Hashtable', Position=0)]
1348 [ValidateNotNullOrEmpty()] [System.Collections.Hashtable[]] $Hashtable,
1349 # Array of PSCustomObjects
1350 [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName='CustomObject', Position=0)]
1351 [ValidateNotNullOrEmpty()] [PSCustomObject[]] $CustomObject,
1352 # Array of Hashtable key names or PSCustomObject property names to include, in display order.
1353 # If not supplied then all Hashtable keys or all PSCustomObject properties will be displayed.
1354 [Parameter(ValueFromPipelineByPropertyName=$true)] [AllowNull()] [string[]] $Columns = $null,
1355 # Array of custom table header strings in display order.
1356 [Parameter(ValueFromPipelineByPropertyName=$true)] [AllowNull()] [string[]] $Headers = $null,
1357 # AutoFit table behavior.
1358 [Parameter(ValueFromPipelineByPropertyName=$true)] [AllowNull()] [int] $AutoFit = -1,
1359 # List view (no headers)
1360 [Switch] $List,
1361 # Grid lines
1362 [Switch] $NoGridLines=$false,
1363 # Built-in Word table formatting style constant
1364 # Would recommend only $wdTableFormatContempory for normal usage (possibly $wdTableFormatList5 for List view)
1365 [Parameter(ValueFromPipelineByPropertyName=$true)] [int] $Format = '-231'
1366 )
1367
1368 Begin
1369 {
1370 Write-Debug ("Using parameter set '{0}'" -f $PSCmdlet.ParameterSetName);
1371 ## Check if -Columns wasn't specified but -Headers were (saves some additional parameter sets!)
1372 If(($Columns -eq $null) -and ($Headers -ne $null))
1373 {
1374 Write-Warning "No columns specified and therefore, specified headers will be ignored.";
1375 $Columns = $null;
1376 }
1377 ElseIf(($Columns -ne $null) -and ($Headers -ne $null))
1378 {
1379 ## Check if number of specified -Columns matches number of specified -Headers
1380 If($Columns.Length -ne $Headers.Length)
1381 {
1382 Write-Error "The specified number of columns does not match the specified number of headers.";
1383 }
1384 } ## end ElseIf
1385 } ## end Begin
1386
1387 Process
1388 {
1389 ## Build the Word table data string to be converted to a range and then a table later.
1390 [System.Text.StringBuilder] $WordRangeString = New-Object System.Text.StringBuilder;
1391
1392 Switch ($PSCmdlet.ParameterSetName)
1393 {
1394 'CustomObject'
1395 {
1396 If($Columns -eq $null)
1397 {
1398 ## Build the available columns from all availble PSCustomObject note properties
1399 [string[]] $Columns = @();
1400 ## Add each NoteProperty name to the array
1401 ForEach($Property in ($CustomObject | Get-Member -MemberType NoteProperty))
1402 {
1403 $Columns += $Property.Name;
1404 }
1405 }
1406
1407 ## Add the table headers from -Headers or -Columns (except when in -List(view)
1408 If(-not $List)
1409 {
1410 Write-Debug ("$(Get-Date): `t`tBuilding table headers");
1411 If($Headers -ne $null)
1412 {
1413 $WordRangeString.AppendFormat("{0}`n", [string]::Join("`t", $Headers));
1414 }
1415 Else
1416 {
1417 $WordRangeString.AppendFormat("{0}`n", [string]::Join("`t", $Columns));
1418 }
1419 }
1420
1421 ## Iterate through each PSCustomObject
1422 Write-Debug ("$(Get-Date): `t`tBuilding table rows");
1423 ForEach($Object in $CustomObject)
1424 {
1425 $OrderedValues = @();
1426 ## Add each row item in the specified order
1427 ForEach($Column in $Columns)
1428 {
1429 $OrderedValues += $Object.$Column;
1430 }
1431 ## Use the ordered list to add each column in specified order
1432 $WordRangeString.AppendFormat("{0}`n", [string]::Join("`t", $OrderedValues));
1433 } ## end ForEach
1434 Write-Debug ("$(Get-Date): `t`t`tAdded '{0}' table rows" -f ($CustomObject.Count));
1435 } ## end CustomObject
1436
1437 Default
1438 { ## Hashtable
1439 If($Columns -eq $null)
1440 {
1441 ## Build the available columns from all available hashtable keys. Hopefully
1442 ## all Hashtables have the same keys (they should for a table).
1443 $Columns = $Hashtable[0].Keys;
1444 }
1445
1446 ## Add the table headers from -Headers or -Columns (except when in -List(view)
1447 If(-not $List)
1448 {
1449 Write-Debug ("$(Get-Date): `t`tBuilding table headers");
1450 If($Headers -ne $null)
1451 {
1452 $WordRangeString.AppendFormat("{0}`n", [string]::Join("`t", $Headers));
1453 }
1454 Else
1455 {
1456 $WordRangeString.AppendFormat("{0}`n", [string]::Join("`t", $Columns));
1457 }
1458 }
1459
1460 ## Iterate through each Hashtable
1461 Write-Debug ("$(Get-Date): `t`tBuilding table rows");
1462 ForEach($Hash in $Hashtable)
1463 {
1464 $OrderedValues = @();
1465 ## Add each row item in the specified order
1466 ForEach($Column in $Columns)
1467 {
1468 $OrderedValues += $Hash.$Column;
1469 }
1470 ## Use the ordered list to add each column in specified order
1471 $WordRangeString.AppendFormat("{0}`n", [string]::Join("`t", $OrderedValues));
1472 } ## end ForEach
1473
1474 Write-Debug ("$(Get-Date): `t`t`tAdded '{0}' table rows" -f $Hashtable.Count);
1475 } ## end default
1476 } ## end Switch
1477
1478 ## Create a MS Word range and set its text to our tab-delimited, concatenated string
1479 Write-Debug ("$(Get-Date): `t`tBuilding table range");
1480 $WordRange = $Script:Doc.Application.Selection.Range;
1481 $WordRange.Text = $WordRangeString.ToString();
1482
1483 ## Create hash table of named arguments to pass to the ConvertToTable method
1484 $ConvertToTableArguments = @{ Separator = [Microsoft.Office.Interop.Word.WdTableFieldSeparator]::wdSeparateByTabs; }
1485
1486 ## Negative built-in styles are not supported by the ConvertToTable method
1487 If($Format -ge 0)
1488 {
1489 $ConvertToTableArguments.Add("Format", $Format);
1490 $ConvertToTableArguments.Add("ApplyBorders", $true);
1491 $ConvertToTableArguments.Add("ApplyShading", $true);
1492 $ConvertToTableArguments.Add("ApplyFont", $true);
1493 $ConvertToTableArguments.Add("ApplyColor", $true);
1494 If(!$List)
1495 {
1496 $ConvertToTableArguments.Add("ApplyHeadingRows", $true);
1497 }
1498 $ConvertToTableArguments.Add("ApplyLastRow", $true);
1499 $ConvertToTableArguments.Add("ApplyFirstColumn", $true);
1500 $ConvertToTableArguments.Add("ApplyLastColumn", $true);
1501 }
1502
1503 ## Invoke ConvertToTable method - with named arguments - to convert Word range to a table
1504 ## See http://msdn.microsoft.com/en-us/library/office/aa171893(v=office.11).aspx
1505 Write-Debug ("$(Get-Date): `t`tConverting range to table");
1506 ## Store the table reference just in case we need to set alternate row coloring
1507 $WordTable = $WordRange.GetType().InvokeMember(
1508 "ConvertToTable", # Method name
1509 [System.Reflection.BindingFlags]::InvokeMethod, # Flags
1510 $null, # Binder
1511 $WordRange, # Target (self!)
1512 ([Object[]]($ConvertToTableArguments.Values)), ## Named argument values
1513 $null, # Modifiers
1514 $null, # Culture
1515 ([String[]]($ConvertToTableArguments.Keys)) ## Named argument names
1516 );
1517
1518 ## Implement grid lines (will wipe out any existing formatting)
1519 If($Format -lt 0)
1520 {
1521 Write-Debug ("$(Get-Date): `t`tSetting table format");
1522 $WordTable.Style = $Format;
1523 }
1524
1525 ## Set the table autofit behavior
1526 If($AutoFit -ne -1)
1527 {
1528 $WordTable.AutoFitBehavior($AutoFit);
1529 }
1530
1531 #the next line causes the heading row to flow across page breaks
1532 $WordTable.Rows.First.Headingformat = $wdHeadingFormatTrue;
1533
1534 If(!$NoGridLines)
1535 {
1536 $WordTable.Borders.InsideLineStyle = $wdLineStyleSingle;
1537 $WordTable.Borders.OutsideLineStyle = $wdLineStyleSingle;
1538 }
1539
1540 Return $WordTable;
1541
1542 } ## end Process
1543}
1544
1545<#
1546.Synopsis
1547 Sets the format of one or more Word table cells
1548.DESCRIPTION
1549 This Function sets the format of one or more table cells, either from a collection
1550 of Word COM object cell references, an individual Word COM object cell reference or
1551 a hashtable containing Row and Column information.
1552
1553 The font name, font size, bold, italic , underline and shading values can be used.
1554.EXAMPLE
1555 SetWordCellFormat -Hashtable $Coordinates -Table $TableReference -Bold
1556
1557 This example sets all text to bold that is contained within the $TableReference
1558 Word table, using an array of hashtables. Each hashtable contain a pair of co-
1559 ordinates that is used to select the required cells. Note: the hashtable must
1560 contain the .Row and .Column key names. For example:
1561 @ { Row = 7; Column = 3 } to set the cell at row 7 and column 3 to bold.
1562.EXAMPLE
1563 $RowCollection = $Table.Rows.First.Cells
1564 SetWordCellFormat -Collection $RowCollection -Bold -Size 10
1565
1566 This example sets all text to size 8 and bold for all cells that are contained
1567 within the first row of the table.
1568 Note: the $Table.Rows.First.Cells Returns a collection of Word COM cells objects
1569 that are in the first table row.
1570.EXAMPLE
1571 $ColumnCollection = $Table.Columns.Item(2).Cells
1572 SetWordCellFormat -Collection $ColumnCollection -BackgroundColor 255
1573
1574 This example sets the background (shading) of all cells in the table's second
1575 column to red.
1576 Note: the $Table.Columns.Item(2).Cells Returns a collection of Word COM cells objects
1577 that are in the table's second column.
1578.EXAMPLE
1579 SetWordCellFormat -Cell $Table.Cell(17,3) -Font "Tahoma" -Color 16711680
1580
1581 This example sets the font to Tahoma and the text color to blue for the cell located
1582 in the table's 17th row and 3rd column.
1583 Note: the $Table.Cell(17,3) Returns a single Word COM cells object.
1584#>
1585Function SetWordCellFormat
1586{
1587 [CmdletBinding(DefaultParameterSetName='Collection')]
1588 Param (
1589 # Word COM object cell collection reference
1590 [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='Collection', Position=0)] [ValidateNotNullOrEmpty()] $Collection,
1591 # Word COM object individual cell reference
1592 [Parameter(Mandatory=$true, ParameterSetName='Cell', Position=0)] [ValidateNotNullOrEmpty()] $Cell,
1593 # Hashtable of cell co-ordinates
1594 [Parameter(Mandatory=$true, ParameterSetName='Hashtable', Position=0)] [ValidateNotNullOrEmpty()] [System.Collections.Hashtable[]] $Coordinates,
1595 # Word COM object table reference
1596 [Parameter(Mandatory=$true, ParameterSetName='Hashtable', Position=1)] [ValidateNotNullOrEmpty()] $Table,
1597 # Font name
1598 [Parameter()] [AllowNull()] [string] $Font = $null,
1599 # Font color
1600 [Parameter()] [AllowNull()] $Color = $null,
1601 # Font size
1602 [Parameter()] [ValidateNotNullOrEmpty()] [int] $Size = 0,
1603 # Cell background color
1604 [Parameter()] [AllowNull()] $BackgroundColor = $null,
1605 # Force solid background color
1606 [Switch] $Solid,
1607 [Switch] $Bold,
1608 [Switch] $Italic,
1609 [Switch] $Underline
1610 )
1611
1612 Begin
1613 {
1614 Write-Debug ("Using parameter set '{0}'." -f $PSCmdlet.ParameterSetName);
1615 }
1616
1617 Process
1618 {
1619 Switch ($PSCmdlet.ParameterSetName)
1620 {
1621 'Collection'
1622 {
1623 ForEach($Cell in $Collection)
1624 {
1625 If($BackgroundColor -ne $null) { $Cell.Shading.BackgroundPatternColor = $BackgroundColor; }
1626 If($Bold) { $Cell.Range.Font.Bold = $true; }
1627 If($Italic) { $Cell.Range.Font.Italic = $true; }
1628 If($Underline) { $Cell.Range.Font.Underline = 1; }
1629 If($Font -ne $null) { $Cell.Range.Font.Name = $Font; }
1630 If($Color -ne $null) { $Cell.Range.Font.Color = $Color; }
1631 If($Size -ne 0) { $Cell.Range.Font.Size = $Size; }
1632 If($Solid) { $Cell.Shading.Texture = 0; } ## wdTextureNone
1633 } # end ForEach
1634 } # end Collection
1635 'Cell'
1636 {
1637 If($Bold) { $Cell.Range.Font.Bold = $true; }
1638 If($Italic) { $Cell.Range.Font.Italic = $true; }
1639 If($Underline) { $Cell.Range.Font.Underline = 1; }
1640 If($Font -ne $null) { $Cell.Range.Font.Name = $Font; }
1641 If($Color -ne $null) { $Cell.Range.Font.Color = $Color; }
1642 If($Size -ne 0) { $Cell.Range.Font.Size = $Size; }
1643 If($BackgroundColor -ne $null) { $Cell.Shading.BackgroundPatternColor = $BackgroundColor; }
1644 If($Solid) { $Cell.Shading.Texture = 0; } ## wdTextureNone
1645 } # end Cell
1646 'Hashtable'
1647 {
1648 ForEach($Coordinate in $Coordinates)
1649 {
1650 $Cell = $Table.Cell($Coordinate.Row, $Coordinate.Column);
1651 If($Bold) { $Cell.Range.Font.Bold = $true; }
1652 If($Italic) { $Cell.Range.Font.Italic = $true; }
1653 If($Underline) { $Cell.Range.Font.Underline = 1; }
1654 If($Font -ne $null) { $Cell.Range.Font.Name = $Font; }
1655 If($Color -ne $null) { $Cell.Range.Font.Color = $Color; }
1656 If($Size -ne 0) { $Cell.Range.Font.Size = $Size; }
1657 If($BackgroundColor -ne $null) { $Cell.Shading.BackgroundPatternColor = $BackgroundColor; }
1658 If($Solid) { $Cell.Shading.Texture = 0; } ## wdTextureNone
1659 }
1660 } # end Hashtable
1661 } # end Switch
1662 } # end process
1663}
1664
1665<#
1666.Synopsis
1667 Sets alternate row colors in a Word table
1668.DESCRIPTION
1669 This Function sets the format of alternate rows within a Word table using the
1670 specified $BackgroundColor. This Function is expensive (in performance terms) as
1671 it recursively sets the format on alternate rows. It would be better to pick one
1672 of the predefined table formats (if one exists)? Obviously the more rows, the
1673 longer it takes :'(
1674
1675 Note: this Function is called by the AddWordTable Function if an alternate row
1676 format is specified.
1677.EXAMPLE
1678 SetWordTableAlternateRowColor -Table $TableReference -BackgroundColor 255
1679
1680 This example sets every-other table (starting with the first) row and sets the
1681 background color to red (wdColorRed).
1682.EXAMPLE
1683 SetWordTableAlternateRowColor -Table $TableReference -BackgroundColor 39423 -Seed Second
1684
1685 This example sets every other table (starting with the second) row and sets the
1686 background color to light orange (weColorLightOrange).
1687#>
1688Function SetWordTableAlternateRowColor
1689{
1690 [CmdletBinding()]
1691 Param (
1692 # Word COM object table reference
1693 [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] [ValidateNotNullOrEmpty()] $Table,
1694 # Alternate row background color
1695 [Parameter(Mandatory=$true, Position=1)] [ValidateNotNull()] [int] $BackgroundColor,
1696 # Alternate row starting seed
1697 [Parameter(ValueFromPipelineByPropertyName=$true, Position=2)] [ValidateSet('First','Second')] [string] $Seed = 'First'
1698 )
1699
1700 Process
1701 {
1702 $StartDateTime = Get-Date;
1703 Write-Debug ("{0}: `t`tSetting alternate table row colors.." -f $StartDateTime);
1704
1705 ## Determine the row seed (only really need to check for 'Second' and default to 'First' otherwise
1706 If($Seed.ToLower() -eq 'second')
1707 {
1708 $StartRowIndex = 2;
1709 }
1710 Else
1711 {
1712 $StartRowIndex = 1;
1713 }
1714
1715 For($AlternateRowIndex = $StartRowIndex; $AlternateRowIndex -lt $Table.Rows.Count; $AlternateRowIndex += 2)
1716 {
1717 $Table.Rows.Item($AlternateRowIndex).Shading.BackgroundPatternColor = $BackgroundColor;
1718 }
1719
1720 ## I've put verbose calls in here we can see how expensive this Functionality actually is.
1721 $EndDateTime = Get-Date;
1722 $ExecutionTime = New-TimeSpan -Start $StartDateTime -End $EndDateTime;
1723 Write-Debug ("{0}: `t`tDone setting alternate row style color in '{1}' seconds" -f $EndDateTime, $ExecutionTime.TotalSeconds);
1724 }
1725}
1726
1727Function ShowScriptOptions
1728{
1729 Write-Verbose "$(Get-Date): "
1730 Write-Verbose "$(Get-Date): "
1731 Write-Verbose "$(Get-Date): Add DateTime : $($AddDateTime)"
1732 Write-Verbose "$(Get-Date): All : $($All)"
1733 If($MSWORD -or $PDF)
1734 {
1735 Write-Verbose "$(Get-Date): Company Name : $($Script:CoName)"
1736 }
1737 Write-Verbose "$(Get-Date): Computers : $($Computers)"
1738 If($MSWORD -or $PDF)
1739 {
1740 Write-Verbose "$(Get-Date): Cover Page : $($CoverPage)"
1741 }
1742 Write-Verbose "$(Get-Date): Dev : $($Dev)"
1743 If($Dev)
1744 {
1745 Write-Verbose "$(Get-Date): DevErrorFile : $($Script:DevErrorFile)"
1746 }
1747 Write-Verbose "$(Get-Date): Filename1 : $($Script:FileName1)"
1748 If($PDF)
1749 {
1750 Write-Verbose "$(Get-Date): Filename2 : $($Script:FileName2)"
1751 }
1752 Write-Verbose "$(Get-Date): Folder : $($Folder)"
1753 Write-Verbose "$(Get-Date): From : $($From)"
1754 Write-Verbose "$(Get-Date): Groups : $($Groups)"
1755 Write-Verbose "$(Get-Date): Log : $($Log)"
1756 Write-Verbose "$(Get-Date): Mgmt : $($Mgmt)"
1757 Write-Verbose "$(Get-Date): Organisational Unit: $($OrganisationalUnit)"
1758 Write-Verbose "$(Get-Date): Save As PDF : $($PDF)"
1759 Write-Verbose "$(Get-Date): Save As WORD : $($MSWORD)"
1760 Write-Verbose "$(Get-Date): Script Info : $($ScriptInfo)"
1761 Write-Verbose "$(Get-Date): Sites : $($Sites)"
1762 Write-Verbose "$(Get-Date): Smtp Port : $($SmtpPort)"
1763 Write-Verbose "$(Get-Date): Smtp Server : $($SmtpServer)"
1764 Write-Verbose "$(Get-Date): To : $($To)"
1765 If($MSWORD -or $PDF)
1766 {
1767 Write-Verbose "$(Get-Date): User Name : $($UserName)"
1768 }
1769 Write-Verbose "$(Get-Date): Users : $($Users)"
1770 Write-Verbose "$(Get-Date): Visible : $($Visible)"
1771 Write-Verbose "$(Get-Date): Use SSL : $($UseSSL)"
1772 Write-Verbose "$(Get-Date): "
1773 Write-Verbose "$(Get-Date): OS Detected : $($Script:RunningOS)"
1774 Write-Verbose "$(Get-Date): PoSH version : $($Host.Version)"
1775 Write-Verbose "$(Get-Date): PSCulture : $($PSCulture)"
1776 Write-Verbose "$(Get-Date): PSUICulture : $($PSUICulture)"
1777 If($MSWORD -or $PDF)
1778 {
1779 Write-Verbose "$(Get-Date): Word language : $($Script:WordLanguageValue)"
1780 Write-Verbose "$(Get-Date): Word version : $($Script:WordProduct)"
1781 }
1782 Write-Verbose "$(Get-Date): "
1783 Write-Verbose "$(Get-Date): Script start : $($Script:StartTime)"
1784 Write-Verbose "$(Get-Date): "
1785 Write-Verbose "$(Get-Date): "
1786}
1787
1788Function validStateProp
1789{
1790 Param(
1791 [object] $object,
1792 [string] $topLevel,
1793 [string] $secondLevel
1794 )
1795
1796 #Function created 8-jan-2014 by Michael B. Smith
1797 If( $object )
1798 {
1799 If( ( Get-Member -Name $topLevel -InputObject $object ) )
1800 {
1801 If( ( Get-Member -Name $secondLevel -InputObject $object.$topLevel ) )
1802 {
1803 Return $True
1804 }
1805 }
1806 }
1807 Return $False
1808}
1809
1810Function SetupWord
1811{
1812 Write-Verbose "$(Get-Date): Setting up Word"
1813
1814 # Setup word for output
1815 Write-Verbose "$(Get-Date): Create Word comObject."
1816 $Script:Word = New-Object -comobject "Word.Application" -EA 0 4>$Null
1817
1818 If(!$? -or $Script:Word -eq $Null)
1819 {
1820 Write-Warning "The Word object could not be created. You may need to repair your Word installation."
1821 $ErrorActionPreference = $SaveEAPreference
1822 Write-Error "`n`n`t`tThe Word object could not be created. You may need to repair your Word installation.`n`n`t`tScript cannot continue.`n`n"
1823 Exit
1824 }
1825
1826 Write-Verbose "$(Get-Date): Determine Word language value"
1827 If( ( validStateProp $Script:Word Language Value__ ) )
1828 {
1829 [int]$Script:WordLanguageValue = [int]$Script:Word.Language.Value__
1830 }
1831 Else
1832 {
1833 [int]$Script:WordLanguageValue = [int]$Script:Word.Language
1834 }
1835
1836 If(!($Script:WordLanguageValue -gt -1))
1837 {
1838 $ErrorActionPreference = $SaveEAPreference
1839 Write-Error "`n`n`t`tUnable to determine the Word language value.`n`n`t`tScript cannot continue.`n`n"
1840 AbortScript
1841 }
1842 Write-Verbose "$(Get-Date): Word language value is $($Script:WordLanguageValue)"
1843
1844 $Script:WordCultureCode = GetCulture $Script:WordLanguageValue
1845
1846 SetWordHashTable $Script:WordCultureCode
1847
1848 [int]$Script:WordVersion = [int]$Script:Word.Version
1849 If($Script:WordVersion -eq $wdWord2016)
1850 {
1851 $Script:WordProduct = "Word 2016"
1852 }
1853 ElseIf($Script:WordVersion -eq $wdWord2013)
1854 {
1855 $Script:WordProduct = "Word 2013"
1856 }
1857 ElseIf($Script:WordVersion -eq $wdWord2010)
1858 {
1859 $Script:WordProduct = "Word 2010"
1860 }
1861 ElseIf($Script:WordVersion -eq $wdWord2007)
1862 {
1863 $ErrorActionPreference = $SaveEAPreference
1864 Write-Error "`n`n`t`tMicrosoft Word 2007 is no longer supported.`n`n`t`tScript will end.`n`n"
1865 AbortScript
1866 }
1867 Else
1868 {
1869 $ErrorActionPreference = $SaveEAPreference
1870 Write-Error "`n`n`t`tYou are running an untested or unsupported version of Microsoft Word.`n`n`t`tScript will end.`n`n`t`tPlease send info on your version of Word to webster@carlwebster.com`n`n"
1871 AbortScript
1872 }
1873
1874 #only validate CompanyName if the field is blank
1875 If([String]::IsNullOrEmpty($Script:CoName))
1876 {
1877 Write-Verbose "$(Get-Date): Company name is blank. Retrieve company name from registry."
1878 $TmpName = ValidateCompanyName
1879
1880 If([String]::IsNullOrEmpty($TmpName))
1881 {
1882 Write-Warning "`n`n`t`tCompany Name is blank so Cover Page will not show a Company Name."
1883 Write-Warning "`n`t`tCheck HKCU:\Software\Microsoft\Office\Common\UserInfo for Company or CompanyName value."
1884 Write-Warning "`n`t`tYou may want to use the -CompanyName parameter if you need a Company Name on the cover page.`n`n"
1885 }
1886 Else
1887 {
1888 $Script:CoName = $TmpName
1889 Write-Verbose "$(Get-Date): Updated company name to $($Script:CoName)"
1890 }
1891 }
1892
1893 If($Script:WordCultureCode -ne "en-")
1894 {
1895 Write-Verbose "$(Get-Date): Check Default Cover Page for $($WordCultureCode)"
1896 [bool]$CPChanged = $False
1897 Switch ($Script:WordCultureCode)
1898 {
1899 'ca-' {
1900 If($CoverPage -eq "Sideline")
1901 {
1902 $CoverPage = "LÃnia lateral"
1903 $CPChanged = $True
1904 }
1905 }
1906
1907 'da-' {
1908 If($CoverPage -eq "Sideline")
1909 {
1910 $CoverPage = "Sidelinje"
1911 $CPChanged = $True
1912 }
1913 }
1914
1915 'de-' {
1916 If($CoverPage -eq "Sideline")
1917 {
1918 $CoverPage = "Randlinie"
1919 $CPChanged = $True
1920 }
1921 }
1922
1923 'es-' {
1924 If($CoverPage -eq "Sideline")
1925 {
1926 $CoverPage = "LÃnea lateral"
1927 $CPChanged = $True
1928 }
1929 }
1930
1931 'fi-' {
1932 If($CoverPage -eq "Sideline")
1933 {
1934 $CoverPage = "Sivussa"
1935 $CPChanged = $True
1936 }
1937 }
1938
1939 'fr-' {
1940 If($CoverPage -eq "Sideline")
1941 {
1942 If($Script:WordVersion -eq $wdWord2013 -or $Script:WordVersion -eq $wdWord2016)
1943 {
1944 $CoverPage = "Lignes latérales"
1945 $CPChanged = $True
1946 }
1947 Else
1948 {
1949 $CoverPage = "Ligne latérale"
1950 $CPChanged = $True
1951 }
1952 }
1953 }
1954
1955 'nb-' {
1956 If($CoverPage -eq "Sideline")
1957 {
1958 $CoverPage = "Sidelinje"
1959 $CPChanged = $True
1960 }
1961 }
1962
1963 'nl-' {
1964 If($CoverPage -eq "Sideline")
1965 {
1966 $CoverPage = "Terzijde"
1967 $CPChanged = $True
1968 }
1969 }
1970
1971 'pt-' {
1972 If($CoverPage -eq "Sideline")
1973 {
1974 $CoverPage = "Linha Lateral"
1975 $CPChanged = $True
1976 }
1977 }
1978
1979 'sv-' {
1980 If($CoverPage -eq "Sideline")
1981 {
1982 $CoverPage = "Sidlinje"
1983 $CPChanged = $True
1984 }
1985 }
1986 }
1987
1988 If($CPChanged)
1989 {
1990 Write-Verbose "$(Get-Date): Changed Default Cover Page from Sideline to $($CoverPage)"
1991 }
1992 }
1993
1994 Write-Verbose "$(Get-Date): Validate cover page $($CoverPage) for culture code $($Script:WordCultureCode)"
1995 [bool]$ValidCP = $False
1996
1997 $ValidCP = ValidateCoverPage $Script:WordVersion $CoverPage $Script:WordCultureCode
1998
1999 If(!$ValidCP)
2000 {
2001 $ErrorActionPreference = $SaveEAPreference
2002 Write-Verbose "$(Get-Date): Word language value $($Script:WordLanguageValue)"
2003 Write-Verbose "$(Get-Date): Culture code $($Script:WordCultureCode)"
2004 Write-Error "`n`n`t`tFor $($Script:WordProduct), $($CoverPage) is not a valid Cover Page option.`n`n`t`tScript cannot continue.`n`n"
2005 AbortScript
2006 }
2007
2008 ShowScriptOptions
2009
2010 $Script:Word.Visible = $False
2011
2012 #http://jdhitsolutions.com/blog/2012/05/san-diego-2012-powershell-deep-dive-slides-and-demos/
2013 #using Jeff's Demo-WordReport.ps1 file for examples
2014 Write-Verbose "$(Get-Date): Load Word Templates"
2015
2016 [bool]$Script:CoverPagesExist = $False
2017 [bool]$BuildingBlocksExist = $False
2018
2019 $Script:Word.Templates.LoadBuildingBlocks()
2020 #word 2010/2013
2021 $BuildingBlocksCollection = $Script:Word.Templates | Where {$_.name -eq "Built-In Building Blocks.dotx"}
2022
2023 Write-Verbose "$(Get-Date): Attempt to load cover page $($CoverPage)"
2024 $part = $Null
2025
2026 $BuildingBlocksCollection | ForEach-Object {
2027 If($_.BuildingBlockEntries.Item($CoverPage).Name -eq $CoverPage)
2028 {
2029 $BuildingBlocks = $_
2030 }
2031 }
2032
2033 If($BuildingBlocks -ne $Null)
2034 {
2035 $BuildingBlocksExist = $True
2036
2037 Try
2038 {
2039 $part = $BuildingBlocks.BuildingBlockEntries.Item($CoverPage)
2040 }
2041
2042 Catch
2043 {
2044 $part = $Null
2045 }
2046
2047 If($part -ne $Null)
2048 {
2049 $Script:CoverPagesExist = $True
2050 }
2051 }
2052
2053 If(!$Script:CoverPagesExist)
2054 {
2055 Write-Verbose "$(Get-Date): Cover Pages are not installed or the Cover Page $($CoverPage) does not exist."
2056 Write-Warning "Cover Pages are not installed or the Cover Page $($CoverPage) does not exist."
2057 Write-Warning "This report will not have a Cover Page."
2058 }
2059
2060 Write-Verbose "$(Get-Date): Create empty word doc"
2061 $Script:Doc = $Script:Word.Documents.Add()
2062 If($Script:Doc -eq $Null)
2063 {
2064 Write-Verbose "$(Get-Date): "
2065 $ErrorActionPreference = $SaveEAPreference
2066 Write-Error "`n`n`t`tAn empty Word document could not be created.`n`n`t`tScript cannot continue.`n`n"
2067 AbortScript
2068 }
2069
2070 $Script:Selection = $Script:Word.Selection
2071 If($Script:Selection -eq $Null)
2072 {
2073 Write-Verbose "$(Get-Date): "
2074 $ErrorActionPreference = $SaveEAPreference
2075 Write-Error "`n`n`t`tAn unknown error happened selecting the entire Word document for default formatting options.`n`n`t`tScript cannot continue.`n`n"
2076 AbortScript
2077 }
2078
2079 #set Default tab stops to 1/2 inch (this line is not from Jeff Hicks)
2080 #36 = .50"
2081 $Script:Word.ActiveDocument.DefaultTabStop = 36
2082
2083 #Disable Spell and Grammar Check to resolve issue and improve performance (from Pat Coughlin)
2084 Write-Verbose "$(Get-Date): Disable grammar and spell checking"
2085 #bug reported 1-Apr-2015 by Tim Mangan
2086 #save current options first before turning them off
2087 $Script:CurrentGrammarOption = $Script:Word.Options.CheckGrammarAsYouType
2088 $Script:CurrentSpellingOption = $Script:Word.Options.CheckSpellingAsYouType
2089 $Script:Word.Options.CheckGrammarAsYouType = $False
2090 $Script:Word.Options.CheckSpellingAsYouType = $False
2091
2092 If($BuildingBlocksExist)
2093 {
2094 #insert new page, getting ready for table of contents
2095 Write-Verbose "$(Get-Date): Insert new page, getting ready for table of contents"
2096 $part.Insert($Script:Selection.Range,$True) | Out-Null
2097 $Script:Selection.InsertNewPage()
2098
2099 #table of contents
2100 Write-Verbose "$(Get-Date): Table of Contents - $($Script:MyHash.Word_TableOfContents)"
2101 $toc = $BuildingBlocks.BuildingBlockEntries.Item($Script:MyHash.Word_TableOfContents)
2102 If($toc -eq $Null)
2103 {
2104 Write-Verbose "$(Get-Date): "
2105 Write-Verbose "$(Get-Date): Table of Content - $($Script:MyHash.Word_TableOfContents) could not be retrieved."
2106 Write-Warning "This report will not have a Table of Contents."
2107 }
2108 Else
2109 {
2110 $toc.insert($Script:Selection.Range,$True) | Out-Null
2111 }
2112 }
2113 Else
2114 {
2115 Write-Verbose "$(Get-Date): Table of Contents are not installed."
2116 Write-Warning "Table of Contents are not installed so this report will not have a Table of Contents."
2117 }
2118
2119 #set the footer
2120 Write-Verbose "$(Get-Date): Set the footer"
2121 [string]$footertext = "Report created by $username"
2122
2123 #get the footer
2124 Write-Verbose "$(Get-Date): Get the footer and format font"
2125 $Script:Doc.ActiveWindow.ActivePane.view.SeekView = $wdSeekPrimaryFooter
2126 #get the footer and format font
2127 $footers = $Script:Doc.Sections.Last.Footers
2128 ForEach($footer in $footers)
2129 {
2130 If($footer.exists)
2131 {
2132 $footer.range.Font.name = "Calibri"
2133 $footer.range.Font.size = 8
2134 $footer.range.Font.Italic = $True
2135 $footer.range.Font.Bold = $True
2136 }
2137 } #end ForEach
2138 Write-Verbose "$(Get-Date): Footer text"
2139 $Script:Selection.HeaderFooter.Range.Text = $footerText
2140
2141 #add page numbering
2142 Write-Verbose "$(Get-Date): Add page numbering"
2143 $Script:Selection.HeaderFooter.PageNumbers.Add($wdAlignPageNumberRight) | Out-Null
2144
2145 FindWordDocumentEnd
2146 Write-Verbose "$(Get-Date):"
2147 #end of Jeff Hicks
2148}
2149
2150Function UpdateDocumentProperties
2151{
2152 Param(
2153 [string] $AbstractTitle,
2154 [string] $SubjectTitle
2155 )
2156
2157 #Update document properties
2158 If($MSWORD -or $PDF)
2159 {
2160 If($Script:CoverPagesExist)
2161 {
2162 Write-Verbose "$(Get-Date): Set Cover Page Properties"
2163 _SetDocumentProperty $Script:Doc.BuiltInDocumentProperties "Company" $Script:CoName
2164 _SetDocumentProperty $Script:Doc.BuiltInDocumentProperties "Title" $Script:title
2165 _SetDocumentProperty $Script:Doc.BuiltInDocumentProperties "Author" $username
2166
2167 _SetDocumentProperty $Script:Doc.BuiltInDocumentProperties "Subject" $SubjectTitle
2168
2169 #Get the Coverpage XML part
2170 $cp = $Script:Doc.CustomXMLParts | Where {$_.NamespaceURI -match "coverPageProps$"}
2171
2172 #get the abstract XML part
2173 $ab = $cp.documentelement.ChildNodes | Where {$_.basename -eq "Abstract"}
2174
2175 #set the text
2176 If([String]::IsNullOrEmpty($Script:CoName))
2177 {
2178 [string]$abstract = $AbstractTitle
2179 }
2180 Else
2181 {
2182 [string]$abstract = "$($AbstractTitle) for $($Script:CoName)"
2183 }
2184
2185 $ab.Text = $abstract
2186
2187 $ab = $cp.documentelement.ChildNodes | Where {$_.basename -eq "PublishDate"}
2188 #set the text
2189 [string]$abstract = (Get-Date -Format d).ToString()
2190 $ab.Text = $abstract
2191
2192 Write-Verbose "$(Get-Date): Update the Table of Contents"
2193 #update the Table of Contents
2194 $Script:Doc.TablesOfContents.item(1).Update()
2195 $cp = $Null
2196 $ab = $Null
2197 $abstract = $Null
2198 }
2199 }
2200}
2201
2202Function SaveandCloseDocumentandShutdownWord
2203{
2204 #bug fix 1-Apr-2014
2205 #reset Grammar and Spelling options back to their original settings
2206 $Script:Word.Options.CheckGrammarAsYouType = $Script:CurrentGrammarOption
2207 $Script:Word.Options.CheckSpellingAsYouType = $Script:CurrentSpellingOption
2208
2209 Write-Verbose "$(Get-Date): Save and Close document and Shutdown Word"
2210 If($Script:WordVersion -eq $wdWord2010)
2211 {
2212 #the $saveFormat below passes StrictMode 2
2213 #I found this at the following two links
2214 #http://blogs.technet.com/b/bshukla/archive/2011/09/27/3347395.aspx
2215 #http://msdn.microsoft.com/en-us/library/microsoft.office.interop.word.wdsaveformat(v=office.14).aspx
2216 If($PDF)
2217 {
2218 Write-Verbose "$(Get-Date): Saving as DOCX file first before saving to PDF"
2219 }
2220 Else
2221 {
2222 Write-Verbose "$(Get-Date): Saving DOCX file"
2223 }
2224 If($AddDateTime)
2225 {
2226 $Script:FileName1 += "_$(Get-Date -f yyyy-MM-dd_HHmm).docx"
2227 If($PDF)
2228 {
2229 $Script:FileName2 += "_$(Get-Date -f yyyy-MM-dd_HHmm).pdf"
2230 }
2231 }
2232 Write-Verbose "$(Get-Date): Running Word 2010 and detected operating system $($Script:RunningOS)"
2233 $saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatDocumentDefault")
2234 $Script:Doc.SaveAs([REF]$Script:FileName1, [ref]$SaveFormat)
2235 If($PDF)
2236 {
2237 Write-Verbose "$(Get-Date): Now saving as PDF"
2238 $saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatPDF")
2239 $Script:Doc.SaveAs([REF]$Script:FileName2, [ref]$saveFormat)
2240 }
2241 }
2242 ElseIf($Script:WordVersion -eq $wdWord2013 -or $Script:WordVersion -eq $wdWord2016)
2243 {
2244 If($PDF)
2245 {
2246 Write-Verbose "$(Get-Date): Saving as DOCX file first before saving to PDF"
2247 }
2248 Else
2249 {
2250 Write-Verbose "$(Get-Date): Saving DOCX file"
2251 }
2252 If($AddDateTime)
2253 {
2254 $Script:FileName1 += "_$(Get-Date -f yyyy-MM-dd_HHmm).docx"
2255 If($PDF)
2256 {
2257 $Script:FileName2 += "_$(Get-Date -f yyyy-MM-dd_HHmm).pdf"
2258 }
2259 }
2260 Write-Verbose "$(Get-Date): Running Word 2013 and detected operating system $($Script:RunningOS)"
2261 $Script:Doc.SaveAs2([REF]$Script:FileName1, [ref]$wdFormatDocumentDefault)
2262 If($PDF)
2263 {
2264 Write-Verbose "$(Get-Date): Now saving as PDF"
2265 $Script:Doc.SaveAs([REF]$Script:FileName2, [ref]$wdFormatPDF)
2266 }
2267 }
2268
2269 Write-Verbose "$(Get-Date): Closing Word"
2270 $Script:Doc.Close()
2271 $Script:Word.Quit()
2272 If($PDF)
2273 {
2274 [int]$cnt = 0
2275 While(Test-Path $Script:FileName1)
2276 {
2277 $cnt++
2278 If($cnt -gt 1)
2279 {
2280 Write-Verbose "$(Get-Date): Waiting another 10 seconds to allow Word to fully close (try # $($cnt))"
2281 Start-Sleep -Seconds 10
2282 $Script:Word.Quit()
2283 If($cnt -gt 2)
2284 {
2285 Stop-WinWord
2286 }
2287 }
2288 Write-Verbose "$(Get-Date): Attempting to delete $($Script:FileName1) since only $($Script:FileName2) is needed (try # $($cnt))"
2289 Remove-Item $Script:FileName1 -EA 0 4>$Null
2290 }
2291 }
2292 Write-Verbose "$(Get-Date): System Cleanup"
2293 [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Script:Word) | Out-Null
2294 If( Test-Path variable:global:Word )
2295 {
2296 Remove-Variable -Name Word -Scope Global 4>$Null
2297 }
2298 If( Get-Variable -Name Word -Scope Script -ErrorAction SilentlyContinue )
2299 {
2300 Remove-Variable -Name Word -Scope Script 4>$Null
2301 }
2302 $SaveFormat = $Null
2303 [gc]::collect()
2304 [gc]::WaitForPendingFinalizers()
2305
2306 Stop-WinWord
2307}
2308
2309Function SetFileName1andFileName2
2310{
2311 Param(
2312 [string] $OutputFileName
2313 )
2314
2315 $pwdPath = $Folder
2316 If($pwdPath -eq "")
2317 {
2318 $pwdpath = $pwd.Path
2319 }
2320
2321 #set $filename1 and $filename2 with no file extension
2322 If($AddDateTime)
2323 {
2324 [string] $Script:FileName1 = Join-Path $pwdPath $OutputFileName
2325 If($PDF)
2326 {
2327 [string] $Script:FileName2 = Join-Path $pwdPath $OutputFileName
2328 }
2329 }
2330
2331 If($MSWord -or $PDF)
2332 {
2333 CheckWordPreReq
2334
2335 If(!$AddDateTime)
2336 {
2337 [string] $Script:FileName1 = ( Join-Path $pwdPath $OutputFileName ) + '.docx'
2338 If($PDF)
2339 {
2340 [string] $Script:FileName2 = ( Join-Path $pwdPath $OutputFileName ) + '.pdf'
2341 }
2342 }
2343
2344 SetupWord
2345 }
2346}
2347
2348#region email Function
2349Function SendEmail
2350{
2351 Param(
2352 [string]$Attachments
2353 )
2354
2355 Write-Verbose "$(Get-Date): Prepare to email"
2356
2357 $emailAttachment = $Attachments
2358 $emailSubject = $Script:Title
2359 $emailBody = @"
2360Hello, <br />
2361<br />
2362$Script:Title is attached.
2363"@
2364
2365 If($Dev)
2366 {
2367 Out-File -FilePath $Script:DevErrorFile -InputObject $error 4>$Null
2368 }
2369
2370 $error.Clear()
2371
2372 If($UseSSL)
2373 {
2374 Write-Verbose "$(Get-Date): Trying to send email using current user's credentials with SSL"
2375 Send-MailMessage -Attachments $emailAttachment -Body $emailBody -BodyAsHtml -From $From `
2376 -Port $SmtpPort -SmtpServer $SmtpServer -Subject $emailSubject -To $To `
2377 -UseSSL *>$Null
2378 }
2379 Else
2380 {
2381 Write-Verbose "$(Get-Date): Trying to send email using current user's credentials without SSL"
2382 Send-MailMessage -Attachments $emailAttachment -Body $emailBody -BodyAsHtml -From $From `
2383 -Port $SmtpPort -SmtpServer $SmtpServer -Subject $emailSubject -To $To *>$Null
2384 }
2385
2386 $e = $error[0]
2387
2388 If($e.Exception.ToString().Contains("5.7.57"))
2389 {
2390 #The server response was: 5.7.57 SMTP; Client was not authenticated to send anonymous mail during MAIL FROM
2391 Write-Verbose "$(Get-Date): Current user's credentials failed. Ask for usable credentials."
2392
2393 If($Dev)
2394 {
2395 Out-File -FilePath $Script:DevErrorFile -InputObject $error -Append 4>$Null
2396 }
2397
2398 $error.Clear()
2399
2400 $emailCredentials = Get-Credential -Message "Enter the email account and password to send email"
2401
2402 If($UseSSL)
2403 {
2404 Send-MailMessage -Attachments $emailAttachment -Body $emailBody -BodyAsHtml -From $From `
2405 -Port $SmtpPort -SmtpServer $SmtpServer -Subject $emailSubject -To $To `
2406 -UseSSL -credential $emailCredentials *>$Null
2407 }
2408 Else
2409 {
2410 Send-MailMessage -Attachments $emailAttachment -Body $emailBody -BodyAsHtml -From $From `
2411 -Port $SmtpPort -SmtpServer $SmtpServer -Subject $emailSubject -To $To `
2412 -credential $emailCredentials *>$Null
2413 }
2414
2415 $e = $error[0]
2416
2417 If($? -and $Null -eq $e)
2418 {
2419 Write-Verbose "$(Get-Date): Email successfully sent using new credentials"
2420 }
2421 Else
2422 {
2423 Write-Verbose "$(Get-Date): Email was not sent:"
2424 Write-Warning "$(Get-Date): Exception: $e.Exception"
2425 }
2426 }
2427 Else
2428 {
2429 Write-Verbose "$(Get-Date): Email was not sent:"
2430 Write-Warning "$(Get-Date): Exception: $e.Exception"
2431 }
2432}
2433#endregion
2434
2435#Script begins
2436
2437$script:startTime = Get-Date
2438
2439#The Function SetFileName1andFileName2 needs your script output filename
2440SetFileName1andFileName2 "ADHealthCheck"
2441
2442#change title for your report
2443[string]$Script:Title = "Active Directory Health Check"
2444
2445###REPLACE AFTER THIS SECTION WITH YOUR SCRIPT###
2446
2447Function Split-IntoGroups
2448{
2449 # Written by 'The Masked Avenger with the Cheetos'
2450 [CmdletBinding()]
2451 param (
2452 [parameter(mandatory=$true,position=0,valuefrompipeline=$true)][Object[]]$InputObject,
2453 [parameter(mandatory=$false,position=1)][ValidateRange(1, ([int]::MaxValue))][int]$Number=10000
2454 )
2455
2456 begin
2457 {
2458 $currentGroup = New-Object System.Collections.ArrayList($Number)
2459 }
2460 process
2461 {
2462 ForEach($object in $InputObject) {
2463 $index = $currentGroup.Add($object)
2464 If($index -ge $Number - 1) {
2465 ,$currentGroup.ToArray()
2466 $currentGroup.Clear()
2467 }
2468 }
2469 }
2470 end
2471 {
2472 If($currentGroup.Count -gt 0) {
2473 ,$currentGroup.ToArray()
2474 }
2475 }
2476}
2477
2478Function Generate-CheckListResults
2479{
2480 [CmdletBinding()]
2481 Param(
2482 [Parameter()]
2483 $Name,
2484
2485 [Parameter()]
2486 $Count
2487 )
2488
2489 $Object = New-Object -TypeName PSObject
2490 $Object | Add-Member -MemberType NoteProperty -Name 'Check' -value $Name
2491 $Object | Add-Member -MemberType NoteProperty -Name 'Results' -value $Count
2492 $Object
2493}
2494
2495$global:someCallers = 0
2496
2497Function Write-ToCSV
2498{
2499 [CmdletBinding()]
2500 Param(
2501 [Parameter( Mandatory = $true, Position = 0, ValuefromPipeline = $true )]
2502 $Content,
2503
2504 [Parameter( Mandatory = $true, Position = 1 )]
2505 [string] $Name,
2506
2507 [Parameter( Mandatory = $false, Position = 2 )]
2508 [string] $Path = $Script:ThisScriptPath
2509 )
2510
2511 $global:someCallers++
2512 If( $null -eq $Content )
2513 {
2514 Write-Debug "***Write-ToCSV: Content is empty, for call count $($global:someCallers)"
2515 Return
2516 }
2517
2518 ## This code makes some assumptions (which were true at the time that
2519 ## the code was written):
2520 ## 1. All entries in $Content are the same type of PSObject.
2521 ## 2. Each entry in $Content is a PSObject.
2522 ## 3. $Content contains at least one entry.
2523 ## 4. PowerShell version 3 or higher.
2524 ## 5. Each PSObject property value is represented in the PSObject
2525 ## by a string or integer.
2526 ## 6. No property values contain a double quote ('"').
2527 ## MBS - 3-May-16
2528
2529 $sample = $null
2530 $count = 0
2531 If( $Content -is [Array] )
2532 {
2533 $sample = $Content[ 0 ]
2534 $count = $Content.Count
2535 }
2536 Else
2537 {
2538 $sample = $Content
2539 $count = 1
2540 }
2541
2542 Write-Debug "***Write-ToCSV: content count $count, call count $($global:someCallers), content type $($content.GetType().Fullname)"
2543
2544 $output = @()
2545 $headers = ''
2546
2547 $properties = $sample.PSObject.Properties
2548 ForEach( $property in $properties )
2549 {
2550 $headers += '"' + $property.Name + '"' + ','
2551 }
2552
2553 $output += $headers.SubString( 0, $headers.Length - 1 )
2554
2555 ForEach( $item in $Content )
2556 {
2557 $properties = $item.PSObject.Properties
2558 $line = ''
2559 ForEach( $property in $properties )
2560 {
2561 $line += '"' + "$($property.Value)" + '"' + ','
2562 }
2563 $output += $line.SubString( 0, $line.Length - 1 )
2564 }
2565
2566 ## $filename = Join-Path "." ( $i.ToString() + '.csv' )
2567 $filename = Join-Path $Path ( $Name + '.csv' )
2568 $output | Out-File $filename -Force -Encoding ascii 4>$Null
2569}
2570
2571Function Write-ToWord
2572{
2573 [CmdletBinding()]
2574 Param(
2575 [Parameter( Mandatory = $true, Position = 0)]
2576 $TableContent,
2577
2578 [Parameter( Mandatory = $true, Position = 1)]
2579 [string]$Name
2580 )
2581
2582 Write-Debug "$(Get-Date): Writing '$Name' to Word"
2583 WriteWordLine -Style 3 -Tabs 0 -Name $Name
2584 FindWordDocumentEnd
2585 $TableContent | Split-IntoGroups | ForEach {
2586 AddWordTable -CustomObject ($TableContent) | Out-Null
2587 FindWordDocumentEnd
2588 WriteWordLine -Style 0 -Tabs 0 -Name ''
2589 }
2590 WriteWordLine -Style 0 -Tabs 0 -Name ''
2591}
2592
2593Function ConvertTo-FQDN
2594{
2595 Param (
2596 [Parameter( Mandatory = $true )]
2597 [string] $DomainFQDN
2598 )
2599
2600 $result = "DC=" + $DomainFQDN.Replace( ".", ",DC=" )
2601 Write-Debug "***ConvertTo-FQDN DomainFQDN='$DomainFQDN', result='$result'"
2602 Return $result
2603}
2604
2605Function Get-Domains
2606{
2607 ( [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() ).Domains
2608}
2609
2610Function Get-ADDomains
2611{
2612 $Domains = Get-Domains
2613 ForEach($Domain in $Domains)
2614 {
2615 $DomainName = $Domain.Name
2616 $DomainFQDN = ConvertTo-FQDN $DomainName
2617
2618 $ADObject = [ADSI]"LDAP://$DomainName"
2619 $sidObject = New-Object System.Security.Principal.SecurityIdentifier( $ADObject.objectSid[ 0 ], 0 )
2620
2621 Write-Debug "***Get-AdDomains DomName='$DomainName', sidObject='$($sidObject.Value)', name='$DomainFQDN'"
2622
2623 $Object = New-Object -TypeName PSObject
2624 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $DomainFQDN
2625 $Object | Add-Member -MemberType NoteProperty -Name 'FQDN' -Value $DomainName
2626 $Object | Add-Member -MemberType NoteProperty -Name 'ObjectSID' -Value $sidObject.Value
2627 $Object
2628 }
2629}
2630
2631Function Get-PrivilegedGroupsMemberCount
2632{
2633 Param (
2634 [Parameter( Mandatory = $true, ValueFromPipeline = $true )]
2635 $Domains
2636 )
2637
2638 ## Jeff W. said this was original code, but until I got ahold of it and
2639 ## rewrote it, it looked only slightly changed from:
2640 ## https://gallery.technet.microsoft.com/scriptcenter/List-Membership-In-bff89703
2641 ## So I give them both credit. :-)
2642
2643 ## the $Domains param is the output from Get-AdDomains above
2644 ForEach( $Domain in $Domains )
2645 {
2646 $DomainSIDValue = $Domain.ObjectSID
2647 $DomainName = $Domain.Name
2648 $DomainFQDN = $Domain.FQDN
2649
2650 Write-Debug "***Get-PrivilegedGroupsMemberCount: domainName='$domainName', domainSid='$domainSidValue'"
2651
2652 ## Carefully chosen from a more complete list at:
2653 ## https://support.microsoft.com/en-us/kb/243330
2654 ## Administrator (not a group, just FYI) - $DomainSidValue-500
2655 ## Domain Admins - $DomainSidValue-512
2656 ## Schema Admins - $DomainSidValue-518
2657 ## Enterprise Admins - $DomainSidValue-519
2658 ## Group Policy Creator Owners - $DomainSidValue-520
2659 ## BUILTIN\Administrators - S-1-5-32-544
2660 ## BUILTIN\Account Operators - S-1-5-32-548
2661 ## BUILTIN\Server Operators - S-1-5-32-549
2662 ## BUILTIN\Print Operators - S-1-5-32-550
2663 ## BUILTIN\Backup Operators - S-1-5-32-551
2664 ## BUILTIN\Replicators - S-1-5-32-552
2665 ## BUILTIN\Network Configuration Operations - S-1-5-32-556
2666 ## BUILTIN\Incoming Forest Trust Builders - S-1-5-32-557
2667 ## BUILTIN\Event Log Readers - S-1-5-32-573
2668 ## BUILTIN\Hyper-V Administrators - S-1-5-32-578
2669 ## BUILTIN\Remote Management Users - S-1-5-32-580
2670
2671 ## FIXME - we report on all these groups for every domain, however
2672 ## some of them are forest wide (thus the membership will be reported
2673 ## in every domain) and some of the groups only exist in the
2674 ## forest root.
2675 $PrivilegedGroups = "$DomainSidValue-512", "$DomainSidValue-518",
2676 "$DomainSidValue-519", "$DomainSidValue-520",
2677 "S-1-5-32-544", "S-1-5-32-548", "S-1-5-32-549",
2678 "S-1-5-32-550", "S-1-5-32-551", "S-1-5-32-552",
2679 "S-1-5-32-556", "S-1-5-32-557", "S-1-5-32-573",
2680 "S-1-5-32-578", "S-1-5-32-580"
2681
2682 ForEach( $PrivilegedGroup in $PrivilegedGroups )
2683 {
2684 $source = New-Object DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
2685 $source.SearchScope = 'Subtree'
2686 $source.PageSize = 1000
2687 $source.Filter = "(objectSID=$PrivilegedGroup)"
2688
2689 Write-Debug "***Get-PrivilegedGroupsMemberCount: LDAP://$DomainName, (objectSid=$PrivilegedGroup)"
2690
2691 $Groups = $source.FindAll()
2692 ForEach( $Group in $Groups )
2693 {
2694 $DistinguishedName = $Group.Properties.Item( 'distinguishedName' )
2695 $groupName = $Group.Properties.Item( 'Name' )
2696
2697 Write-Debug "***Get-PrivilegedGroupsMemberCount: searching group '$groupName'"
2698
2699 $Source.Filter = "(memberOf:1.2.840.113556.1.4.1941:=$DistinguishedName)"
2700 $Users = $null
2701 ## CHECK: I don't think a try/catch is necessary here - MBS
2702 try
2703 {
2704 $Users = $Source.FindAll()
2705 }
2706 catch
2707 {
2708 # nothing
2709 }
2710 If( $null -eq $users )
2711 {
2712 ## Obsolete: F-I-X-M-E: we should probably Return a PSObject with a count of zero
2713 ## Write-ToCSV and Write-ToWord understand empty Return results.
2714
2715 Write-Debug "***Get-PrivilegedGroupsMemberCount: no members found in $groupName"
2716 }
2717 Else
2718 {
2719 Function GetProperValue
2720 {
2721 Param(
2722 [Object] $object
2723 )
2724
2725 If( $object -is [System.DirectoryServices.SearchResultCollection] )
2726 {
2727 Return $object.Count
2728 }
2729 If( $object -is [System.DirectoryServices.SearchResult] )
2730 {
2731 Return 1
2732 }
2733 If( $object -is [Array] )
2734 {
2735 Return $object.Count
2736 }
2737 If( $null -eq $object )
2738 {
2739 Return 0
2740 }
2741
2742 Return 1
2743 }
2744
2745 [int]$script:MemberCount = GetProperValue $Users
2746
2747 Write-Debug "***Get-PrivilegedGroupsMemberCount: '$groupName' user count before first filter $MemberCount"
2748
2749 $Object = New-Object -TypeName PSObject
2750 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
2751 $Object | Add-Member -MemberType NoteProperty -Name 'Group' -Value $groupName
2752
2753 $Members = $Users | Where-Object { $_.Properties.Item( 'objectCategory' ).Item( 0 ) -like 'cn=person*' }
2754 $script:MemberCount = GetProperValue $Members
2755
2756 Write-Debug "***Get-PrivilegedGroupsMemberCount: '$groupName' user count after first filter $MemberCount"
2757
2758 Write-Debug "***Get-PrivilegedGroupsMemberCount: '$groupName' has $MemberCount members"
2759
2760 $Object | Add-Member -MemberType NoteProperty -Name 'Members' -Value $MemberCount
2761 $Object
2762 }
2763 }
2764 }
2765 }
2766}
2767
2768Function Get-AllADDomainControllers
2769{
2770 [CmdletBinding()]
2771 Param (
2772 [Parameter( Mandatory = $true, ValueFromPipeline = $true )]
2773 $Domain
2774 )
2775
2776 $DomainName = $Domain.Name
2777 $DomainFQDN = $Domain.FQDN
2778
2779 $adsiSearcher = New-Object DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
2780 $adsiSearcher.Filter = '(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))'
2781 $Servers = $adsiSearcher.FindAll()
2782
2783 ForEach( $Server in $Servers )
2784 {
2785 $dcName = $Server.Properties.item( 'Name' )
2786
2787 Write-Debug "***Get-AllAdDomainControllers DomainName='$DomainName', DomainFQDN='$($DomainFQDN)', DCname='$dcName'"
2788
2789 $Object = New-Object -TypeName PSObject
2790 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
2791 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $dcName
2792 $Object | Add-Member -MemberType NoteProperty -Name 'LastContact' -Value $Server.Properties.Item( 'whenchanged' )
2793 $Object
2794 }
2795}
2796
2797Function Get-AllADMemberServers
2798{
2799 [CmdletBinding()]
2800 Param (
2801 [Parameter( Mandatory = $true, ValueFromPipeline = $true )]
2802 $Domain
2803 )
2804
2805 $DomainName = $Domain.Name
2806 $DomainFQDN = $Domain.FQDN
2807
2808 Write-Debug "***Enter: Get-AllAdMemberServers DomainName='$domainName'"
2809
2810 $adsiSearcher = New-Object DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
2811 $adsiSearcher.Filter = '(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))"'
2812 $Servers = $adsiSearcher.FindAll()
2813
2814 If( $null -eq $servers )
2815 {
2816 Write-Debug '***Get-AllAdMemberServers: no member servers were found'
2817 Return
2818 }
2819
2820 ForEach( $Server in $Servers )
2821 {
2822 $serverName = $Server.Properties.Item( 'Name' )
2823
2824 Write-Debug "***Get-AllAdMemberServers DomainName='$DomainName', DomainFQDN='$DomainFQDN', serverName='$serverName'"
2825
2826 $Object = New-Object -TypeName PSObject
2827 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
2828 $Object | Add-Member -MemberType NoteProperty -Name 'ComputerName' -Value $serverName
2829 $Object
2830 }
2831}
2832
2833Function Get-AllADMemberServerObjects
2834{
2835 [CmdletBinding()]
2836 Param (
2837 [Parameter( Mandatory = $true, Parametersetname = 'PasswordNeverExpires' )]
2838 [Switch]$PasswordNeverExpires,
2839
2840 [Parameter( Mandatory = $true, Parametersetname = 'PasswordExpiration' )]
2841 [int]$PasswordExpiration,
2842
2843 [Parameter( Mandatory = $true, Parametersetname = 'AccountNeverExpires' )]
2844 [Switch]$AccountNeverExpires,
2845
2846 [Parameter( Mandatory = $true, Parametersetname = 'Disabled' )]
2847 [Switch]$Disabled,
2848
2849 [Parameter( Mandatory = $true, Position = 1, ValueFromPipeline = $true, Parametersetname = 'PasswordNeverExpires' )]
2850 [Parameter( Mandatory = $true, Position = 1, ValueFromPipeline = $true, Parametersetname = 'PasswordExpiration' )]
2851 [Parameter( Mandatory = $true, Position = 1, ValueFromPipeline = $true, Parametersetname = 'AccountNeverExpires' )]
2852 [Parameter( Mandatory = $true, Position = 1, ValueFromPipeline = $true, Parametersetname = 'Disabled' )]
2853 $Domain
2854 )
2855
2856 $DomainName = $Domain.Name
2857 $DomainFQDN = $Domain.FQDN
2858 $localParamset = $PSCmdlet.ParameterSetName
2859
2860 Write-Debug "***Enter Get-AllADMemberServerObjects, DomainName='$DomainName', ParamSet='$localParamset'"
2861
2862 $source = New-Object System.DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
2863 $source.SearchScope = 'Subtree'
2864 $source.PageSize = 1000
2865
2866 Switch ( $localParamset )
2867 {
2868 'PasswordNeverExpires'
2869 {
2870 $source.Filter = "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192))(userAccountControl:1.2.840.113556.1.4.803:=65536))"
2871 }
2872 'PasswordExpiration'
2873 {
2874 $source.Filter = "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))"
2875 }
2876 'AccountNeverExpires'
2877 {
2878 $source.Filter = "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192))(|(accountExpires=0)(accountExpires=9223372036854775807)))"
2879 }
2880 'Disabled'
2881 {
2882 #$source.Filter = "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8194)))"
2883 $source.Filter = "(&(&(objectCategory=computer)(objectClass=computer)(operatingSystem=*server*)(useraccountcontrol:1.2.840.113556.1.4.803:=2)))"
2884 }
2885 }
2886
2887 If( $localParamset -eq 'PasswordExpiration' )
2888 {
2889 try
2890 {
2891 $source.FindAll() | ForEach-Object {
2892 $fileTime = $null
2893 $passLast = $_.Properties[ 'PwdLastSet' ].Item( 0 )
2894 If( $null -ne $passLast )
2895 {
2896 $fileTime = [DateTime]::FromFileTime( $passLast )
2897 }
2898
2899 If( $null -ne $passLast -and
2900 $fileTime -lt ( [DateTime]::Now ).AddMonths( -$PasswordExpiration ) )
2901 {
2902 $serverName = $_.Properties.Item( 'Name' )
2903
2904 Write-Debug "***Get-AllADMemberServerObjects, paramset='$localParamset', found server='$serverName'"
2905
2906 $Object = New-Object -TypeName PSObject
2907 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
2908 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $serverName
2909 $Object | Add-Member -MemberType NoteProperty -Name 'PasswordLastSet' -Value $fileTime
2910 $Object
2911 }
2912 }
2913 }
2914 catch
2915 {
2916 }
2917 }
2918 Else
2919 {
2920 try
2921 {
2922 $source.FindAll() | ForEach-Object {
2923 $serverName = $_.Properties.Item( 'Name' )
2924
2925 Write-Debug "***Get-AllADMemberServerObjects, paramset='$localParamset', found server='$serverName'"
2926
2927 $Object = New-Object -TypeName PSObject
2928 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
2929 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $serverName
2930 $Object
2931 }
2932 }
2933 catch
2934 {
2935 }
2936 }
2937}
2938
2939Function Get-ADUserObjects
2940{
2941 [CmdletBinding()]
2942 Param (
2943 [Parameter( Mandatory = $true, Parametersetname = 'PasswordNeverExpires')]
2944 [Switch]$PasswordNeverExpires,
2945
2946 [Parameter( Mandatory = $true, Parametersetname = 'PasswordNotRequired')]
2947 [Switch]$PasswordNotRequired,
2948
2949 [Parameter( Mandatory = $true, Parametersetname = 'PasswordChangeAtNextLogon')]
2950 [Switch]$PasswordChangeAtNextLogon,
2951
2952 [Parameter( Mandatory = $true, Parametersetname = 'PasswordExpiration')]
2953 [int]$PasswordExpiration,
2954
2955 [Parameter( Mandatory = $true, Parametersetname = 'NotRequireKerbereosAuthentication')]
2956 [Switch]$NotRequireKerbereosAuthentication,
2957
2958 [Parameter( Mandatory = $true, Parametersetname = 'AccountNoExpire')]
2959 [Switch]$AccountNoExpire,
2960
2961 [Parameter( Mandatory = $true, Parametersetname = 'Disabled')]
2962 [Switch]$Disabled,
2963
2964 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'PasswordNeverExpires' )]
2965 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'PasswordNotRequired' )]
2966 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'PasswordChangeAtNextLogon' )]
2967 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'PasswordExpiration' )]
2968 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'NotRequireKerbereosAuthentication' )]
2969 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'AccountNoExpire' )]
2970 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, Parametersetname = 'Disabled' )]
2971 $Domain
2972 )
2973
2974 ## this doesn't know how to process passwordSettingsObjects (fine-grained passwords) -- FIXME
2975
2976 $DomainName = $Domain.Name
2977 $DomainFQDN = $Domain.FQDN
2978 $localParamset = $PSCmdlet.ParameterSetName
2979
2980 Write-Debug "***Enter Get-ADUserObjects: domain='$DomainName', paramset='$localParamset'"
2981
2982 $source = New-Object System.Directoryservices.Directorysearcher( "LDAP://$DomainName" )
2983 $source.SearchScope = 'Subtree'
2984 $source.PageSize = 1000
2985
2986 Switch ( $localParamset )
2987 {
2988 'PasswordNeverExpires'
2989 {
2990 $source.filter = "(&(sAMAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
2991 }
2992 'PasswordNotRequired'
2993 {
2994 $source.filter = "(&(sAMAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=32))"
2995 }
2996 'PasswordChangeAtNextLogon'
2997 {
2998 $source.filter = "(&(sAMAccountType=805306368)(pwdLastSet=0))"
2999 }
3000 'PasswordExpiration'
3001 {
3002 $source.filter = "(&(sAMAccountType=805306368)(pwdLastSet>=0))"
3003 }
3004 'NotRequireKerbereosAuthentication'
3005 {
3006 $source.filter = "(&(sAMAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"
3007 }
3008 'AccountNoExpire'
3009 {
3010 $source.filter = "(&(sAMAccountType=805306368)(|(accountExpires=0)(accountExpires=9223372036854775807)))"
3011 }
3012 'Disabled'
3013 {
3014 $source.filter = "(&(sAMAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=2))"
3015 }
3016 }
3017
3018 If( $localParamset -eq 'PasswordExpiration' )
3019 {
3020 try
3021 {
3022 $source.FindAll() | ForEach-Object {
3023 $fileTime = $null
3024 $passLast = $_.Properties[ 'PwdLastSet' ].Item( 0 )
3025 If( $null -ne $passLast )
3026 {
3027 $fileTime = [DateTime]::FromFileTime( $passLast )
3028 }
3029
3030 If( $null -ne $passLast -and
3031 $fileTime -lt ( [DateTime]::Now ).AddMonths( -$PasswordExpiration ) )
3032 {
3033 $userName = $_.Properties.Item( 'Name' )
3034
3035 Write-Debug "***Get-ADUserObjects: domain='$DomainFQDN', paramset='$localParamset', username='$userName'"
3036
3037 $Object = New-Object -TypeName PSObject
3038 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3039 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $userName
3040 $Object | Add-Member -MemberType NoteProperty -Name 'PasswordLastSet' -Value $fileTime
3041 $Object
3042 }
3043 }
3044 }
3045 catch
3046 {
3047 }
3048 }
3049 Else
3050 {
3051 try
3052 {
3053 $source.FindAll() | ForEach-Object {
3054 $userName = $_.Properties.Item( 'Name' )
3055
3056 Write-Debug "***Get-ADUserObjects: domain='$DomainFQDN', paramset='$localParamset', username='$userName'"
3057
3058 $Object = New-Object -TypeName PSObject
3059 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3060 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $userName
3061 $Object
3062 }
3063 }
3064 catch
3065 {
3066 }
3067 }
3068}
3069
3070Function Get-OUGPInheritanceBlocked
3071{
3072 [CmdletBinding()]
3073 Param (
3074 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3075 $Domain
3076 )
3077
3078 $DomainName = $Domain.Name
3079 $DomainFQDN = $Domain.FQDN
3080
3081 Write-Debug "***Enter: Get-OUGPInheritanceBlocked, DomainName '$DomainName'"
3082
3083 $source = New-Object System.DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
3084 $source.SearchScope = 'Subtree'
3085 $source.PageSize = 1000
3086 $source.filter = '(&(objectclass=OrganizationalUnit)(gpoptions=1))'
3087 try
3088 {
3089 $source.FindAll() | ForEach-Object {
3090 $ouName = $_.Properties.Item( 'Name' )
3091
3092 Write-Debug "***Get-OuGpInheritanceBlocked: Inheritance blocked on OU '$ouName' in domain '$DomainName'"
3093
3094 $Object = New-Object -TypeName PSObject
3095 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3096 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $ouName
3097 $Object
3098 }
3099 }
3100 catch
3101 {
3102 }
3103}
3104
3105Function Get-ADSites
3106{
3107 [CmdletBinding()]
3108 Param (
3109 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3110 $Domain
3111 )
3112
3113 $DomainName = $Domain.Name
3114 $DomainFQDN = $Domain.FQDN
3115 $searchRoot = "LDAP://CN=Sites,CN=Configuration,$DomainName"
3116
3117 Write-Debug "***Enter: Get-AdSites, DomainName='$($DomainName)', SearchRoot='$searchRoot'"
3118
3119 $source = New-Object System.DirectoryServices.DirectorySearcher
3120 $source.SearchScope = 'Subtree'
3121 $source.SearchRoot = $searchRoot
3122 $source.PageSize = 1000
3123 $source.Filter = '(objectclass=site)'
3124
3125 try
3126 {
3127 $source.FindAll() | ForEach-Object {
3128 $siteName = $_.Properties.Item( 'Name' )
3129 $desc = $_.Properties.Item( 'Description' )
3130
3131 If( [String]::IsNullOrEmpty( $desc ) )
3132 {
3133 $desc = ' '
3134 }
3135
3136 Write-Debug "***Get-AdSites: domainFQDN='$DomainFQDN', sitename='$sitename', desc='$desc'"
3137
3138 $subnets = @()
3139 $siteBL = $_.Properties.Item( 'siteObjectBL' )
3140 ForEach( $item in $siteBL )
3141 {
3142 $temp = $item.SubString( 0, $item.IndexOf( ',' ) ) ## up to first ","
3143 $temp = $temp.SubString( 3 ) ## drop CN=
3144
3145 Write-Debug "***Get-AdSites: sitename='$sitename', subnet='$temp'"
3146
3147 $subnets += $temp
3148 }
3149 If( $subnets.Count -eq 0 )
3150 {
3151 $subnets = $null
3152 }
3153
3154 $Object = New-Object -TypeName PSObject
3155 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3156 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $siteName
3157 $Object | Add-Member -MemberType NoteProperty -Name 'Description' -Value $desc
3158 $Object | Add-Member -MemberType NoteProperty -Name 'Subnets' -Value $subnets
3159 $Object
3160 }
3161 }
3162 catch
3163 {
3164 }
3165}
3166
3167Function Get-ADSiteServer
3168{
3169 [CmdletBinding()]
3170 Param (
3171 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
3172 $Domain,
3173
3174 [Parameter( Mandatory = $true )]
3175 $Site
3176 )
3177
3178 $DomainName = $Domain.Name
3179 $DomainFQDN = $Domain.FQDN
3180 $searchRoot = "LDAP://CN=Servers,CN=$Site,CN=Sites,CN=Configuration,$DomainName"
3181
3182 Write-Debug "***Enter: Get-AdSiteServer DomainName='$domainName', DomainFQDN='$domainFQDN', searchRoot='$searchRoot'"
3183
3184 $source = New-Object System.DirectoryServices.DirectorySearcher
3185 $source.SearchRoot = $searchRoot
3186 $source.SearchScope = 'Subtree'
3187 $source.PageSize = 1000
3188 $source.Filter = '(objectclass=server)'
3189
3190 try
3191 {
3192 $SiteServers = $source.FindAll()
3193 If( $null -ne $SiteServers )
3194 {
3195 ForEach( $SiteServer in $SiteServers )
3196 {
3197 $serverName = $SiteServer.Properties.Item( 'Name' )
3198
3199 Write-Debug "***Get-AdSiteServer: serverName='$serverName' found in site '$site' in domain '$domainFQDN'"
3200
3201 $Object = New-Object -TypeName PSObject
3202 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3203 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $Site
3204 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $serverName
3205 $Object
3206 }
3207 }
3208 Else
3209 {
3210 Write-Debug "***Get-AdSiteServer: No server found in site '$site' in domain '$domainFQDN'"
3211
3212 $Object = New-Object -TypeName PSObject
3213 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3214 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $Site
3215 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value ' '
3216 $Object
3217 }
3218 }
3219 catch
3220 {
3221 }
3222}
3223
3224Function Get-ADSiteConnection
3225{
3226 [CmdletBinding()]
3227 Param (
3228 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3229 $Domain,
3230
3231 [Parameter( Mandatory = $true )]
3232 $Site
3233 )
3234
3235 $DomainName = $Domain.Name
3236 $DomainFQDN = $Domain.FQDN
3237 $searchRoot = "LDAP://CN=$Site,CN=Sites,CN=Configuration,$DomainName"
3238
3239 Write-Debug "***Enter: Get-ADSiteConnection DomainName='$DomainName', DomainFQDN='$DomainFQDN', searchRoot='$searchRoot'"
3240
3241 $source = New-Object System.DirectoryServices.DirectorySearcher
3242 $source.SearchRoot = $searchRoot
3243 $source.SearchScope = 'Subtree'
3244 $source.PageSize = 1000
3245 $source.Filter = '(objectclass=nTDSConnection)'
3246
3247 try
3248 {
3249 $SiteConnections = $source.FindAll()
3250 If( $null -ne $SiteConnections )
3251 {
3252 ForEach( $SiteConnection in $SiteConnections )
3253 {
3254 $connectName = $SiteConnection.Properties.Item( 'Name' )
3255 $connectServer = $SiteConnection.Properties.Item( 'FromServer' )
3256
3257 Write-Debug "***Get-ADSiteConnection DomainFQDN='$DomainFQDN', site='$Site', connectionName='$connectName'"
3258
3259 $Object = New-Object -TypeName PSObject
3260 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3261 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $Site
3262 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $connectName
3263 $Object | Add-Member -MemberType NoteProperty -Name 'FromServer' -Value $($connectServer -split ',' -replace 'CN=','')[3]
3264 $Object
3265 }
3266 }
3267 Else
3268 {
3269 Write-Debug "***Get-ADSiteConnection DomainFQDN='$DomainFQDN', site='$Site', no connections"
3270
3271 $Object = New-Object -TypeName PSObject
3272 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3273 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $Site
3274 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value ' '
3275 $Object | Add-Member -MemberType NoteProperty -Name 'FromServer' -Value ' '
3276 $Object
3277 }
3278 }
3279 catch
3280 {
3281 }
3282}
3283
3284Function Get-ADSiteLink
3285{
3286 [CmdletBinding()]
3287 Param (
3288 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3289 $Domain
3290 )
3291
3292 $DomainName = $Domain.Name
3293 $DomainFQDN = $Domain.FQDN
3294 $searchRoot = "LDAP://CN=Sites,CN=Configuration,$DomainName"
3295
3296 Write-Debug "***Enter: Get-AdSiteLink DomainName='$DomainName', DomainFQDN='$DomainFQDN', searchRoot='$searchRoot'"
3297
3298 $source = New-Object System.DirectoryServices.DirectorySearcher
3299 $source.SearchRoot = $searchRoot
3300 $source.SearchScope = 'Subtree'
3301 $source.PageSize = 1000
3302 $source.Filter = '(objectclass=sitelink)'
3303
3304 try
3305 {
3306 $SiteLinks = $source.FindAll()
3307 ForEach( $SiteLink in $SiteLinks )
3308 {
3309 $siteLinkName = $SiteLink.Properties.Item( 'Name' )
3310 $siteLinkDesc = $SiteLink.Properties.Item( 'Description' )
3311 $siteLinkRepl = $SiteLink.Properties.Item( 'replinterval' )
3312 $siteLinkSite = $SiteLink.Properties.Item( 'Sitelist' )
3313 $siteLinkCt = 0
3314
3315 If( $siteLinkSite )
3316 {
3317 $siteLinkCt = $siteLinkSite.Count
3318 }
3319
3320 $sites = @()
3321 ForEach( $item in $siteLinkSite )
3322 {
3323 $temp = $item.SubString( 0, $item.IndexOf( ',' ) )
3324 $temp = $temp.SubString( 3 )
3325 $sites += $temp
3326 }
3327 If( $sites.Count -eq 0 )
3328 {
3329 $sites = $null
3330 $siteLinkCt = 0
3331 }
3332
3333 Write-Debug "***Get-AdSiteLink: Name='$siteLinkName', Desc='$siteLinkDesc', Repl='$siteLinkRepl', Count='$siteLinkCt'"
3334
3335 If( [String]::IsNullOrEmpty( $siteLinkDesc ) )
3336 {
3337 $siteLinkDesc = ' '
3338 }
3339
3340 If( $null -ne $sites )
3341 {
3342 ForEach( $Site in $Sites )
3343 {
3344 Write-Debug "***Get-AdSiteLink: siteLinkName='$siteLinkName', sitename='$site'"
3345
3346 $Object = New-Object -TypeName PSObject
3347 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3348 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $siteLinkName
3349 $Object | Add-Member -MemberType NoteProperty -Name 'Description' -Value $siteLinkDesc
3350 $Object | Add-Member -MemberType NoteProperty -Name 'Replication Interval' -Value $siteLinkRepl
3351 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $site
3352 $Object | Add-Member -MemberType NoteProperty -Name 'Site Count' -Value $siteLinkCt
3353 $Object
3354 }
3355 }
3356 Else
3357 {
3358 Write-Debug "***Get-AdSiteLink: siteLinkName='$siteLinkName', siteName='<empty>'"
3359
3360 $Object = New-Object -TypeName PSObject
3361 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3362 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $siteLinkName
3363 $Object | Add-Member -MemberType NoteProperty -Name 'Description' -Value $siteLinkDesc
3364 $Object | Add-Member -MemberType NoteProperty -Name 'Replication Interval' -Value $siteLinkRepl
3365 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value ' '
3366 $Object | Add-Member -MemberType NoteProperty -Name 'Site Count' -Value '0'
3367 $Object
3368 }
3369 }
3370 }
3371 catch
3372 {
3373 }
3374}
3375
3376Function Get-ADSiteSubnet
3377{
3378 [CmdletBinding()]
3379 Param (
3380 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3381 $Domain
3382 )
3383
3384 $DomainName = $Domain.Name
3385 $DomainFQDN = $Domain.FQDN
3386 $searchRoot = "LDAP://CN=Subnets,CN=Sites,CN=Configuration,$DomainName"
3387
3388 Write-Debug "***Enter Get-AdSiteSubnet DomainName='$DomainName', DomainFQDN='$DomainFQDN', searchRoot='$searchRoot'"
3389
3390 $source = New-Object System.DirectoryServices.DirectorySearcher
3391 $source.SearchRoot = $searchRoot
3392 $source.SearchScope = 'Subtree'
3393 $source.PageSize = 1000
3394 $source.Filter = '(objectclass=subnet)'
3395
3396 try
3397 {
3398 $source.FindAll() | ForEach-Object {
3399 $subnetSite = ($_.Properties.Item( 'SiteObject' ) -split ',' -replace 'CN=','')[0]
3400 $subnetName = $_.Properties.Item( 'Name' )
3401 $subnetDesc = $_.Properties.Item( 'Description' )
3402
3403 Write-Debug "***Get-AdSiteSubnet: site='$subnetSite', name='$subnetName', desc='$subnetDesc'"
3404
3405 $Object = New-Object -TypeName PSObject
3406 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3407 $Object | Add-Member -MemberType NoteProperty -Name 'Site' -Value $subnetSite
3408 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $subnetName
3409 $Object | Add-Member -MemberType NoteProperty -Name 'Description' -Value $subnetDesc
3410 $Object
3411 }
3412 }
3413 catch
3414 {
3415 }
3416}
3417
3418Function Get-ADEmptyGroups
3419{
3420 [CmdletBinding()]
3421 Param (
3422 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3423 $Domain
3424 )
3425
3426 ## $exclude includes (punny, aren't I?) the list of groups commonly used as a
3427 ## 'Primary Group' in Active Directory. While, theoretically, ANY group can be
3428 ## a primary group, that is quite rare.
3429 $exclude = 'Domain Users', 'Domain Computers', 'Domain Controllers', 'Domain Guests'
3430
3431 $DomainName = $Domain.Name
3432 $DomainFQDN = $Domain.FQDN
3433
3434 Write-Debug "***Enter Get-AdEmptyGroups DomainName='$DomainName', DomainFQDN='$DomainFQDN'"
3435
3436 $source = New-Object DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
3437 $source.SearchScope = 'Subtree'
3438 $source.PageSize = 1000
3439 $source.Filter = '(&(objectCategory=Group)(!member=*))'
3440
3441 try
3442 {
3443 $groups = $source.FindAll()
3444 $groups = (($groups | ? { $exclude -notcontains $_.Properties[ 'Name' ].Item( 0 ) } ) | % { $_.Properties[ 'Name' ].Item( 0 ) }) | sort
3445 ForEach( $group in $groups )
3446 {
3447 Write-Debug "***Get-AdEmptyGroups: DomainFQDN='$DomainFQDN', empty groupname='$group'"
3448
3449 $Object = New-Object -TypeName PSObject
3450 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3451 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $group
3452 $Object
3453 }
3454 }
3455 catch
3456 {
3457 }
3458}
3459
3460Function Get-ADDomainLocalGroups
3461{
3462 [CmdletBinding()]
3463 Param (
3464 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3465 $Domain
3466 )
3467
3468 $DomainName = $Domain.Name
3469 $DomainFQDN = $Domain.FQDN
3470
3471 Write-Debug "***Enter Get-AdDomainLocalGroups DomainName='$DomainName', DomainFQDN='$DomainFQDN'"
3472
3473 $search = New-Object System.DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
3474 $search.SearchScope = 'Subtree'
3475 $search.PageSize = 1000
3476 $search.Filter = '(&(groupType:1.2.840.113556.1.4.803:=4)(!(groupType:1.2.840.113556.1.4.803:=1)))'
3477
3478 try
3479 {
3480 $search.FindAll() | ForEach-Object {
3481 $groupName = $_.Properties.Item( 'Name' )
3482 $groupDN = $_.Properties.Item( 'Distinguishedname' )
3483
3484 Write-Debug "***Get-AdDomainLocalGroups groupName='$groupName', dn='$groupDN'"
3485
3486 $Object = New-Object -TypeName PSObject
3487 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3488 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $groupName
3489 $Object | Add-Member -MemberType NoteProperty -Name 'DistinguishedName' -Value $groupDN
3490 $Object
3491 }
3492 }
3493 catch
3494 {
3495 }
3496}
3497
3498Function Get-ADUsersInDomainLocalGroups
3499{
3500 [CmdletBinding()]
3501 Param (
3502 [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
3503 $Domain
3504 )
3505
3506 $DomainName = $Domain.Name
3507 $DomainFQDN = $Domain.FQDN
3508
3509 Write-Debug "***Enter Get-AdUsersInDomainLocalGroups DomainName='$DomainName', DomainFQDN='$DomainFQDN'"
3510
3511 $search = New-Object DirectoryServices.DirectorySearcher( "LDAP://$DomainName" )
3512 $search.SearchScope = 'Subtree'
3513 $search.PageSize = 1000
3514 $search.Filter = '(&(groupType:1.2.840.113556.1.4.803:=4)(!(groupType:1.2.840.113556.1.4.803:=1)))'
3515
3516 try
3517 {
3518 ## $search was being used twice.
3519 $results = $search.FindAll()
3520 $results | ForEach-Object {
3521 $groupName = $_.Properties.Item( 'Name' )
3522 $DistinguishedName = $_.Properties.Item( 'DistinguishedName' )
3523
3524 Write-Debug "***Get-AdUsersInDomainLocalGroups name='$groupName', dn='$distinguishedName'"
3525
3526 $search.Filter = "(&(memberOf=$DistinguishedName)(objectclass=User))"
3527 $search.FindAll() | ForEach-Object {
3528 $userName = $_.Properties.Item( 'Name' )
3529
3530 Write-Debug "***Get-AdUsersInDomainLocalGroups name='$groupName', user='$userName'"
3531
3532 $Object = New-Object -TypeName PSObject
3533 $Object | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $DomainFQDN
3534 $Object | Add-Member -MemberType NoteProperty -Name 'Group' -Value $groupName
3535 $Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $userName
3536 $Object
3537 }
3538 }
3539 }
3540 catch
3541 {
3542 }
3543}
3544
3545#region process document output
3546Function ProcessDocumentOutput
3547{
3548 If($MSWORD -or $PDF)
3549 {
3550 SaveandCloseDocumentandShutdownWord
3551 }
3552
3553 Write-Verbose "$(Get-Date): Script has completed"
3554 Write-Verbose "$(Get-Date): "
3555
3556 $GotFile = $False
3557
3558 If($PDF)
3559 {
3560 If(Test-Path "$($Script:FileName2)")
3561 {
3562 Write-Verbose "$(Get-Date): $($Script:FileName2) is ready for use"
3563 Write-Verbose "$(Get-Date): "
3564 $GotFile = $True
3565 }
3566 Else
3567 {
3568 Write-Warning "$(Get-Date): Unable to save the output file, $($Script:FileName2)"
3569 Write-Error "Unable to save the output file, $($Script:FileName2)"
3570 }
3571 }
3572 Else
3573 {
3574 If(Test-Path "$($Script:FileName1)")
3575 {
3576 Write-Verbose "$(Get-Date): $($Script:FileName1) is ready for use"
3577 Write-Verbose "$(Get-Date): "
3578 $GotFile = $True
3579 }
3580 Else
3581 {
3582 Write-Warning "$(Get-Date): Unable to save the output file, $($Script:FileName1)"
3583 Write-Error "Unable to save the output file, $($Script:FileName1)"
3584 }
3585 }
3586
3587 #email output file if requested
3588 If($GotFile -and ![System.String]::IsNullOrEmpty( $SmtpServer ))
3589 {
3590 If($PDF)
3591 {
3592 $emailAttachment = $Script:FileName2
3593 }
3594 Else
3595 {
3596 $emailAttachment = $Script:FileName1
3597 }
3598 SendEmail $emailAttachment
3599 }
3600
3601 Write-Verbose "$(Get-Date): "
3602}
3603#endregion
3604
3605#region end script
3606Function ProcessScriptEnd
3607{
3608 #http://poshtips.com/measuring-elapsed-time-in-powershell/
3609 Write-Verbose "$(Get-Date): Script started: $($Script:StartTime)"
3610 Write-Verbose "$(Get-Date): Script ended: $(Get-Date)"
3611 $runtime = $(Get-Date) - $Script:StartTime
3612 $Str = [string]::format("{0} days, {1} hours, {2} minutes, {3}.{4} seconds",
3613 $runtime.Days,
3614 $runtime.Hours,
3615 $runtime.Minutes,
3616 $runtime.Seconds,
3617 $runtime.Milliseconds)
3618 Write-Verbose "$(Get-Date): Elapsed time: $($Str)"
3619
3620 If($Dev)
3621 {
3622 If($SmtpServer -eq "")
3623 {
3624 Out-File -FilePath $Script:DevErrorFile -InputObject $error 4>$Null
3625 }
3626 Else
3627 {
3628 Out-File -FilePath $Script:DevErrorFile -InputObject $error -Append 4>$Null
3629 }
3630 }
3631
3632 If($ScriptInfo)
3633 {
3634 Out-File -FilePath $Script:SIFile -InputObject "" 4>$Null
3635 Out-File -FilePath $Script:SIFile -Append -InputObject "Add DateTime : $($AddDateTime)" 4>$Null
3636 Out-File -FilePath $Script:SIFile -Append -InputObject "All : $($All)" 4>$Null
3637 If($MSWORD -or $PDF)
3638 {
3639 Out-File -FilePath $Script:SIFile -Append -InputObject "Company Name : $($Script:CoName)" 4>$Null
3640 }
3641 Out-File -FilePath $Script:SIFile -Append -InputObject "Computers : $($computers)" 4>$Null
3642 If($MSWORD -or $PDF)
3643 {
3644 Out-File -FilePath $Script:SIFile -Append -InputObject "Cover Page : $($CoverPage)" 4>$Null
3645 }
3646 Out-File -FilePath $Script:SIFile -Append -InputObject "Dev : $($Dev)" 4>$Null
3647 If($Dev)
3648 {
3649 Out-File -FilePath $Script:SIFile -Append -InputObject "DevErrorFile : $($Script:DevErrorFile)" 4>$Null
3650 }
3651 Out-File -FilePath $Script:SIFile -Append -InputObject "Filename1 : $($Script:FileName1)" 4>$Null
3652 If($PDF)
3653 {
3654 Out-File -FilePath $Script:SIFile -Append -InputObject "Filename2 : $($Script:FileName2)" 4>$Null
3655 }
3656 Out-File -FilePath $Script:SIFile -Append -InputObject "Folder : $($Folder)" 4>$Null
3657 Out-File -FilePath $Script:SIFile -Append -InputObject "From : $($From)" 4>$Null
3658 Out-File -FilePath $Script:SIFile -Append -InputObject "Groups : $($groups)" 4>$Null
3659 Out-File -FilePath $Script:SIFile -Append -InputObject "Log : $($Log)" 4>$Null
3660 Out-File -FilePath $Script:SIFile -Append -InputObject "Mgmt : $($mgmt)" 4>$Null
3661 Out-File -FilePath $Script:SIFile -Append -InputObject "Organisational Unit: $($OrganisationalUnit)" 4>$Null
3662 Out-File -FilePath $Script:SIFile -Append -InputObject "Save As PDF : $($PDF)" 4>$Null
3663 Out-File -FilePath $Script:SIFile -Append -InputObject "Save As WORD : $($MSWORD)" 4>$Null
3664 Out-File -FilePath $Script:SIFile -Append -InputObject "Script Info : $($ScriptInfo)" 4>$Null
3665 Out-File -FilePath $Script:SIFile -Append -InputObject "Sites : $($Sites)" 4>$Null
3666 Out-File -FilePath $Script:SIFile -Append -InputObject "Smtp Port : $($SmtpPort)" 4>$Null
3667 Out-File -FilePath $Script:SIFile -Append -InputObject "Smtp Server : $($SmtpServer)" 4>$Null
3668 Out-File -FilePath $Script:SIFile -Append -InputObject "To : $($To)" 4>$Null
3669 Out-File -FilePath $Script:SIFile -Append -InputObject "Use SSL : $($UseSSL)" 4>$Null
3670 If($MSWORD -or $PDF)
3671 {
3672 Out-File -FilePath $Script:SIFile -Append -InputObject "User Name : $($UserName)" 4>$Null
3673 }
3674 Out-File -FilePath $Script:SIFile -Append -InputObject "Users : $($users)" 4>$Null
3675 Out-File -FilePath $Script:SIFile -Append -InputObject "Visible : $($Visible)" 4>$Null
3676 Out-File -FilePath $Script:SIFile -Append -InputObject "" 4>$Null
3677 Out-File -FilePath $Script:SIFile -Append -InputObject "OS Detected : $($Script:RunningOS)" 4>$Null
3678 Out-File -FilePath $Script:SIFile -Append -InputObject "PoSH version : $($Host.Version)" 4>$Null
3679 Out-File -FilePath $Script:SIFile -Append -InputObject "PSUICulture : $($PSUICulture)" 4>$Null
3680 Out-File -FilePath $Script:SIFile -Append -InputObject "PSCulture : $($PSCulture)" 4>$Null
3681 If($MSWORD -or $PDF)
3682 {
3683 Out-File -FilePath $Script:SIFile -Append -InputObject "Word language : $($Script:WordLanguageValue)" 4>$Null
3684 Out-File -FilePath $Script:SIFile -Append -InputObject "Word version : $($Script:WordProduct)" 4>$Null
3685 }
3686 Out-File -FilePath $Script:SIFile -Append -InputObject "" 4>$Null
3687 Out-File -FilePath $Script:SIFile -Append -InputObject "Script start : $($Script:StartTime)" 4>$Null
3688 Out-File -FilePath $Script:SIFile -Append -InputObject "Elapsed time : $($Str)" 4>$Null
3689 }
3690
3691 $runtime = $Null
3692 $Str = $Null
3693 $ErrorActionPreference = $SaveEAPreference
3694}
3695#endregion
3696
3697#region Content
3698$Script:MgmtPage = @()
3699
3700Function Generate-TableContent
3701{
3702 [CmdletBinding()]
3703 Param(
3704 $content,
3705 $hashParam,
3706 $title
3707 )
3708
3709 $count = 0
3710 If( $null -eq $content )
3711 {
3712 ## do not early-Return, because the MgmtPage needs to be updated
3713 Write-Debug "***Generate-TableContent: empty for title='$title'"
3714 }
3715 Else
3716 {
3717 $count = 1
3718 If( $content -is [Array] )
3719 {
3720 $count = $content.Count
3721 }
3722
3723 Write-Debug "***Generate-TableContent: count=$count for title='$title'"
3724
3725 If( $hashParam.ContainsKey( 'CSV' ) )
3726 {
3727 Write-ToCSV -Name $title -Content $content
3728 }
3729 Write-ToWord -Name $title -TableContent $content
3730 }
3731
3732 If( $hashParam.ContainsKey( 'Mgmt' ) )
3733 {
3734 $script:MgmtPage += Generate-CheckListResults -Name $title -Count $count
3735 }
3736}
3737
3738Function IsInDomain
3739{
3740 $computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue -verbose:$False
3741 If( !$? -or $null -eq $computerSystem )
3742 {
3743 $computerSystem = Get-WmiObject Win32_ComputerSystem -ErrorAction SilentlyContinue
3744 If( !$? -or $null -eq $computerSystem )
3745 {
3746 Write-Error 'IsInDomain: fatal error: cannot obtain Win32_ComputerSystem from CIM or WMI.'
3747 AbortScript
3748 }
3749 }
3750
3751 Return $computerSystem.PartOfDomain
3752}
3753
3754If( -not ( IsInDomain ) )
3755{
3756 Write-Error 'ADHealthCheck must be run from a computer that is a member of a domain.'
3757 AbortScript
3758}
3759
3760FindWordDocumentEnd
3761$Script:Selection.InsertNewPage()
3762Write-Verbose "$(Get-Date): Get domains"
3763$Domains = Get-ADDomains
3764If( $null -eq $Domains )
3765{
3766 Write-Error 'ADHealthCheck cannot obtain a list of domains in the forest.'
3767 AbortScript
3768}
3769
3770$parameters = $PSBoundParameters
3771$paramset = $PSCmdlet.ParameterSetName
3772
3773ForEach( $Domain in $Domains )
3774{
3775 $DomainFQDN = $Domain.FQDN
3776 Write-Verbose "$(Get-Date): Domain $DomainFQDN"
3777 WriteWordLine -Style 1 -Tabs 0 -Name "Domain $DomainFQDN"
3778 FindWordDocumentEnd
3779 If(($parameters.ContainsKey('Sites')) -or ($paramset -eq 'All') -or ($paramset -eq 'SMTP'))
3780 {
3781 #Sites
3782 $Script:Selection.InsertNewPage()
3783 FindWordDocumentEnd
3784 Write-Verbose "$(Get-Date): Sites"
3785 WriteWordLine -Style 2 -Tabs 0 -Name 'Sites'
3786 FindWordDocumentEnd
3787 $TableContentTemp = Get-ADSites -Domain $Domain
3788
3789 #Sites - Description empty
3790 $CheckTitle = 'Sites - Without a description'
3791 Write-Verbose "$(Get-Date): $CheckTitle"
3792 If($TableContentTemp -ne $null)
3793 {
3794 $TableContent = $TableContentTemp | Where-Object {$_.Description -eq $null}
3795 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3796
3797 #Sites - No subnet
3798 $CheckTitle = 'Sites - Without one or more subnet(s)'
3799 Write-Verbose "$(Get-Date): $CheckTitle"
3800 $TableContent = $TableContentTemp | Where-Object {$_.Subnets -eq $null}
3801 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3802
3803 #Sites - No server
3804 $CheckTitle = 'Sites - No server(s)'
3805 Write-Verbose "$(Get-Date): $CheckTitle"
3806 $TableContent = $TableContentTemp | ForEach-Object { Get-ADSiteServer -Site $_.Site -Domain $Domain } | Where-Object {$_.Name -eq $null}
3807 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3808
3809 #Sites - No connection
3810 $CheckTitle = 'Sites - Without a connection'
3811 Write-Verbose "$(Get-Date): $CheckTitle"
3812 $TableContent = $TableContentTemp | ForEach-Object { Get-ADSiteConnection -Site $_.site -Domain $Domain } | Where-Object {$_.Name -eq $null}
3813 WriteWordLine -Style 3 -Tabs 0 -Name $CheckTitle
3814 FindWordDocumentEnd
3815 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3816 WriteWordLine -Style 0 -Tabs 0 -Name ''
3817 FindWordDocumentEnd
3818 }
3819
3820 $allSiteLinks = Get-AdSiteLink -Domain $Domain
3821
3822 #Sites - No sitelink
3823 $CheckTitle = 'Sites - No sitelink(s)'
3824 Write-Verbose "$(Get-Date): $CheckTitle"
3825 $TableContent = $allSiteLinks | Where-Object {$_.'Site Count' -eq '0'}
3826 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3827
3828 #Sitelinks - One site
3829 $CheckTitle = 'Sites - With one sitelink'
3830 Write-Verbose "$(Get-Date): $CheckTitle"
3831 $TableContent = $allSiteLinks | Where-Object {$_.'Site Count' -eq '1'}
3832 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3833
3834 #Sitelinks - More than two sites
3835 $CheckTitle = 'SiteLinks - More than two sitelinks'
3836 Write-Verbose "$(Get-Date): $CheckTitle"
3837 $TableContent = $allSiteLinks | Where-Object {$_.'Site Count' -gt '2'}
3838 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3839
3840 #Sitelinks - No description
3841 $CheckTitle = 'SiteLinks - Without a description'
3842 Write-Verbose "$(Get-Date): $CheckTitle"
3843 $TableContent = $allSiteLinks | Where-Object {$_.Description -eq $null}
3844 Generate-TableContent $TableContent $PSBoundParameters $CheckTitle
3845
3846 #ADSubnets - Available but not in use
3847 $CheckTitle = 'Subnets in Sites - Not in use'
3848 Write-Verbose "$(Get-Date): $CheckTitle"
3849 $AvailableSubnets = Get-ADSiteSubnet -Domain $Domain | select -ExpandProperty 'name'
3850 $InUseSubnets = Get-ADSites -Domain $Domain | select -ExpandProperty 'subnets'
3851 If(($AvailableSubnets -ne $Null) -and ($InUseSubnets -ne $null))
3852 {
3853 $TableContent = Compare-Object -DifferenceObject $InUseSubnets -ReferenceObject $AvailableSubnets
3854 Generate-TableContent $TableContent $parameters $CheckTitle
3855 }
3856 }
3857 If(($parameters.ContainsKey('OrganisationalUnit')) -or ($paramset -eq 'All') -or ($paramset -eq 'SMTP'))
3858 {
3859 #OrganisationalUnit
3860 $Script:Selection.InsertNewPage()
3861 FindWordDocumentEnd
3862 Write-Verbose "$(Get-Date): OU"
3863 WriteWordLine -Style 2 -Tabs 0 -Name 'Organisational Units'
3864 ## FIXME - no organizational units shown
3865 #OU - GPO inheritance blocked
3866 $CheckTitle = 'OU - GPO inheritance blocked'
3867 Write-Verbose "$(Get-Date): $CheckTitle"
3868 $TableContent = Get-OUGPInheritanceBlocked -Domain $Domain
3869 WriteWordLine -Style 3 -Tabs 0 -Name $CheckTitle
3870 FindWordDocumentEnd
3871 Generate-TableContent $TableContent $parameters $CheckTitle
3872 }
3873 If(($parameters.ContainsKey('Computers')) -or ($paramset -eq 'All') -or ($paramset -eq 'SMTP'))
3874 {
3875 #Domain Controllers
3876 $Script:Selection.InsertNewPage()
3877 FindWordDocumentEnd
3878 Write-Verbose "$(Get-Date): Domain Controllers"
3879 WriteWordLine -Style 2 -Tabs 0 -Name 'Domain Controllers'
3880 ## FIXME - write all domain controller names? Domain? OS Version? Etc.
3881 FindWordDocumentEnd
3882
3883 #Domain Controllers - No contact
3884 $CheckTitle = 'Domain Controllers - No contact in the last 3 months'
3885 Write-Verbose "$(Get-Date): $CheckTitle"
3886 $TableContent = Get-AllADDomainControllers -Domain $Domain | Where-Object {$_.LastContact -lt (([datetime]::Now).AddMonths(-6))} | Sort-Object -Property LastContact -Descending
3887 WriteWordLine -Style 3 -Tabs 0 -Name $CheckTitle
3888 FindWordDocumentEnd
3889 Generate-TableContent $TableContent $parameters $CheckTitle
3890 WriteWordLine -Style 0 -Tabs 0 -Name ''
3891 FindWordDocumentEnd
3892
3893 #Member Servers
3894 Write-Verbose "$(Get-Date): Member Servers"
3895 WriteWordLine -Style 2 -Tabs 0 -Name 'Member Servers'
3896 FindWordDocumentEnd
3897
3898 #Member Servers - Password never expires
3899 $CheckTitle = 'Member Servers - Password never expires'
3900 Write-Verbose "$(Get-Date): $CheckTitle"
3901 $TableContent = Get-AllADMemberServerObjects -Domain $Domain -PasswordNeverExpires | Sort -Property Name
3902 Generate-TableContent $TableContent $parameters $CheckTitle
3903
3904 #Computers - Password expired
3905 $CheckTitle = 'Member Servers - Password more than 6 months old'
3906 Write-Verbose "$(Get-Date): $CheckTitle"
3907 $TableContent = Get-AllADMemberServerObjects -Domain $Domain -PasswordExpiration '6' | Sort -Property Name
3908 Generate-TableContent $TableContent $parameters $CheckTitle
3909
3910 #Member Servers - Account never expires
3911 $CheckTitle = 'Member Servers - Account never expires'
3912 Write-Verbose "$(Get-Date): $CheckTitle"
3913 $TableContent = Get-AllADMemberServerObjects -Domain $Domain -AccountNeverExpires | Sort -Property Name
3914 Generate-TableContent $TableContent $parameters $CheckTitle
3915
3916 #Member Servers - Account disabled
3917 $CheckTitle = 'Member Servers - Account disabled'
3918 Write-Verbose "$(Get-Date): $CheckTitle"
3919 $TableContent = Get-AllADMemberServerObjects -Domain $Domain -Disabled | Sort -Property Name
3920 Generate-TableContent $TableContent $parameters $CheckTitle
3921 }
3922
3923 If(($parameters.ContainsKey('Users')) -or ($paramset -eq 'All') -or ($paramset -eq 'SMTP'))
3924 {
3925 #Users
3926 $Script:Selection.InsertNewPage()
3927 FindWordDocumentEnd
3928 Write-Verbose "$(Get-Date): Users"
3929 WriteWordLine -Style 2 -Tabs 0 -Name 'Users'
3930 FindWordDocumentEnd
3931
3932 #Users in Domain Local Groups
3933 $CheckTitle = 'Users - Direct member of a Domain Local Group'
3934 Write-Verbose "$(Get-Date): $CheckTitle"
3935 $TableContent = Get-ADUsersInDomainLocalGroups -Domain $Domain | Sort -Property Group, Name
3936 Generate-TableContent $TableContent $parameters $CheckTitle
3937
3938 #Users - Password never expires
3939 $CheckTitle = 'Users - Password never expires'
3940 Write-Verbose "$(Get-Date): $CheckTitle"
3941 $TableContent = Get-ADUserObjects -Domain $Domain -PasswordNeverExpires | Sort -Property Name
3942 Generate-TableContent $TableContent $parameters $CheckTitle
3943
3944 #Users - Password not required
3945 $CheckTitle = 'Users - Password not required'
3946 Write-Verbose "$(Get-Date): $CheckTitle"
3947 $TableContent = Get-ADUserObjects -Domain $Domain -PasswordNotRequired | Sort -Property Name
3948 Generate-TableContent $TableContent $parameters $CheckTitle
3949
3950 #Users - Password needs to be changed at next logon
3951 $CheckTitle = 'Users - Change password at next logon'
3952 Write-Verbose "$(Get-Date): $CheckTitle"
3953 $TableContent = Get-ADUserObjects -Domain $Domain -PasswordChangeAtNextLogon | Sort -Property Name
3954 Generate-TableContent $TableContent $parameters $CheckTitle
3955
3956 #Users - Password not changed in last 12 months
3957 $CheckTitle = 'Users - Password not changed in last 12 months'
3958 Write-Verbose "$(Get-Date): $CheckTitle"
3959 $TableContent = Get-ADUserObjects -Domain $Domain -PasswordExpiration '12' | Sort -Property Name
3960 Generate-TableContent $TableContent $parameters $CheckTitle
3961
3962 #Users - Account without expiration date
3963 $CheckTitle = 'Users - Account without expiration date'
3964 Write-Verbose "$(Get-Date): $CheckTitle"
3965 $TableContent = Get-ADUserObjects -Domain $Domain -AccountNoExpire | Sort -Property Name
3966 Generate-TableContent $TableContent $parameters $CheckTitle
3967
3968 #Users - Do not require kerberos preauthentication
3969 $CheckTitle = 'Users - Do not require kerberos preauthentication'
3970 Write-Verbose "$(Get-Date): $CheckTitle"
3971 $TableContent = Get-ADUserObjects -Domain $Domain -NotRequireKerbereosAuthentication | Sort -Property Name
3972 Generate-TableContent $TableContent $parameters $CheckTitle
3973
3974 #Users - Disabled
3975 $CheckTitle = 'Users - Disabled'
3976 Write-Verbose "$(Get-Date): $CheckTitle"
3977 $TableContent = Get-ADUserObjects -Domain $Domain -Disabled | Sort -Property Name
3978 Generate-TableContent $TableContent $parameters $CheckTitle
3979 }
3980
3981 If(($parameters.ContainsKey('Groups')) -or ($paramset -eq 'All') -or ($paramset -eq 'SMTP'))
3982 {
3983 #Groups
3984 Write-Verbose "$(Get-Date): Groups"
3985 $Script:Selection.InsertNewPage()
3986 FindWordDocumentEnd
3987 WriteWordLine -Style 2 -Tabs 0 -Name 'Groups'
3988 FindWordDocumentEnd
3989 #Privileged Groups
3990 Write-Verbose "$(Get-Date): Groups - Privileged groups"
3991 $TableContentTemp = Get-PrivilegedGroupsMemberCount -Domains $Domain | Sort -Property Group
3992
3993 #Groups - Privileged with many members
3994 $CheckTitle = 'Groups - Privileged - More than 5 members'
3995 Write-Verbose "$(Get-Date): $CheckTitle"
3996 If($TableContentTemp -ne $null)
3997 {
3998 $TableContent = $TableContentTemp | Where {$_.Members -gt '5'} | Sort -Property Group
3999 Generate-TableContent $TableContent $parameters $CheckTitle
4000 }
4001
4002 #Groups - Privileged with no members
4003 $CheckTitle = 'Groups - Privileged - No members'
4004 Write-Verbose "$(Get-Date): $CheckTitle"
4005 If($TableContentTemp -ne $null)
4006 {
4007 $TableContent = $TableContentTemp | Where {$_.Members -eq '0'} | Sort -Property Group
4008 Generate-TableContent $TableContent $parameters $CheckTitle
4009 }
4010
4011 #Groups - Empty
4012 $CheckTitle = 'Groups - Primary - Empty (no members)'
4013 Write-Verbose "$(Get-Date): $CheckTitle"
4014 $TableContent = Get-ADEmptyGroups -Domain $Domain | Sort -Property Name
4015 Generate-TableContent $TableContent $parameters $CheckTitle
4016 }
4017
4018 $CheckTitle = 'Management'
4019 Write-Verbose "$(Get-Date): $CheckTitle"
4020 If($parameters.ContainsKey('Mgmt'))
4021 {
4022 If($parameters.ContainsKey('CSV'))
4023 {
4024 Write-ToCSV -Name $CheckTitle -Content $MgmtPage
4025 }
4026 $Script:Selection.InsertNewPage()
4027 FindWordDocumentEnd
4028 WriteWordLine -Style 2 -Tabs 0 -Name $CheckTitle
4029 FindWordDocumentEnd
4030 Write-ToWord -Name 'Management Table' -TableContent $MgmtPage
4031 }
4032}
4033#endregion Content
4034
4035###REPLACE BEFORE THIS SECTION WITH YOUR SCRIPT###
4036
4037Write-Verbose "$(Get-Date): Finishing up document"
4038#end of document processing
4039
4040###Change the two lines below for your script
4041$AbstractTitle = "AD Health Check Report"
4042$SubjectTitle = "Active Directory Health Check Report"
4043UpdateDocumentProperties $AbstractTitle $SubjectTitle
4044
4045ProcessDocumentOutput
4046
4047ProcessScriptEnd
4048
4049If($parameters.ContainsKey('Log'))
4050{
4051 If($Script:StartLog -eq $true)
4052 {
4053 try
4054 {
4055 Stop-Transcript | Out-Null
4056 Write-Verbose "$(Get-Date): $Script:LogPath is ready for use"
4057 }
4058 catch
4059 {
4060 Write-Verbose "$(Get-Date): Transcript/log stop failed"
4061 }
4062 }
4063}