functions/new-domainreport.ps1
#todo show disabled accounts in orange or red Function New-ADDomainReport { [cmdletbinding()] [outputtype("System.IO.File")] Param ( [Parameter(Position = 0, HelpMessage = "Specify the domain name. The default is the user domain.")] [ValidateNotNullOrEmpty()] [alias("domain")] [string]$Name = $env:USERDOMAIN, [Parameter(Mandatory, HelpMessage = "Specify the output HTML file.")] [ValidateScript( { #validate the parent folder Test-Path (Split-Path $_) })] [string]$FilePath, [Parameter(HelpMessage = "Enter the name of the report to be displayed in the web browser")] [ValidateNotNullOrEmpty()] [string]$ReportTitle = "Domain Report", [Parameter(HelpMessage = "Specify the path the CSS file. If you don't specify one, the default module file will be used.")] [ValidateScript( { Test-Path $_ })] [string]$CSSPath = "$PSScriptRoot\..\reports\domainreport.css", [Parameter(HelpMessage = "Embed the CSS file into the HTML document head. You can only embed from a file, not a URL.")] [switch]$EmbedCSS, [Parameter(HelpMessage = "Specify a domain controller to query.")] [alias("dc", "domaincontroller")] [string]$Server, [Parameter(HelpMessage = "Specify an alternate credential.")] [alias("RunAs")] [PSCredential]$Credential ) Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)" #region setup $progParams = @{ Activity = $myinvocation.MyCommand Status = "Preparing" CurrentOperation = "Initializing variables" } Write-Progress @progParams #set some default parameter values $params = "Credential", "Server" ForEach ($param in $params) { if ($PSBoundParameters.ContainsKey($param)) { Write-Verbose "[$((Get-Date).TimeofDay)] Adding 'Get-AD*:$param' to script PSDefaultParameterValues" $script:PSDefaultParameterValues["Get-AD*:$param"] = $PSBoundParameters.Item($param) } } #foreach #some report metadata $cmd = Get-Command $myinvocation.InvocationName $thisScript = "{0}\{1}" -f $cmd.source, $cmd.name $reportVersion = ( $cmd).version.tostring() #who is running the report? if ($Credential.Username) { $who = $Credential.UserName } else { $who = "$($env:USERDOMAIN)\$($env:USERNAME)" } #where are they running the report from? Try { #disable verbose output from Resolve-DNSName $where = (Resolve-DnsName -Name $env:COMPUTERNAME -Type A -ErrorAction Stop -Verbose:$False).Name | Select-Object -Last 1 } Catch { $where = $env:COMPUTERNAME } $post = @" <table class='footer'> <tr align = "right"><td>Report run: <i>$(Get-Date)</i></td></tr> <tr align = "right"><td>Author: <i>$($Who.toUpper())</i></td></tr> <tr align = "right"><td>Source: <i>$thisScript</i></td></tr> <tr align = "right"><td>Version: <i>$ReportVersion</i></td></tr> <tr align = "right"><td>Computername: <i>$($where.toUpper())</i></td></tr> </table> "@ $head = @" <title>$ReportTitle</Title> <style> td[tip]:hover { color: #2112f1cc; position: relative; } td[tip]:hover:after { content: attr(tip); left: 0; top: 100%; margin-left: 80px; margin-top: 10px; width: 400px; padding: 3px 8px; position: absolute; color: #111111; font-family: 'Courier New', Courier, monospace; font-size: 10pt; background-color: rgba(210, 212, 198, 0.897); white-space: pre-wrap; } th.dn { width:40%; } </style> <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js'> </script> <script type='text/javascript'> function toggleDiv(divId) { `$("#"+divId).toggle(); } function toggleAll() { var divs = document.getElementsByTagName('div'); for (var i = 0; i < divs.length; i++) { var div = divs[i]; `$("#"+div.id).toggle(); } } </script> "@ #parameters to splat to ConvertTo-Html $cHtml = @{ Head = $head Body = "" PostContent = $post } If ($EmbedCSS) { if (Test-Path -Path $CSSPath) { Write-Verbose "[$((Get-Date).TimeofDay)] Embedding CSS content from $CSSPath" $cssContent = Get-Content -Path $CssPath | Where-Object { $_ -notmatch "^@" } $head += @" <style> $cssContent </style> "@ #update the hashtable $cHtml.Head = $head } else { Write-Error "Failed to find a CSS file at $CSSPath. You can only embed from a file." #bail out Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)" return } } #if embedCSS else { $cHtml.add("CSSUri", $cssPath) } #endregion #region get data Write-Verbose "[$((Get-Date).TimeofDay)] Getting base information for $Name" $progParams.status = "Getting domain information" $progParams.CurrentOperation = " Get-ADDomain -Identity $Name" Write-Progress @progParams Try { $root = Get-ADDomain -Identity $Name -ErrorAction stop } Catch { Write-Warning "Failed to get domain information for $Name. $($_.Exception.message)" Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)" #bail out return } $progParams.CurrentOperation = "Get-ADBranch -searchbase $root.DistinguishedName" Write-Progress @progParams Write-Verbose "[$((Get-Date).TimeofDay)] Getting ADBranch data for $($root.distinguishedname)" $dom = Get-ADBranch -searchbase $root.DistinguishedName $fragments = [System.Collections.Generic.List[string]]::New() $fragments.Add("<H1>$($root.dnsroot)</H1>") $fragments.Add("<a href='javascript:toggleAll();' title='Click to toggle all sections'>+/-</a>") $grouped = $dom | Sort-Object -Property Parent | Group-Object -Property Parent $progParams.Status = "Processing domain branches" foreach ($branch in $grouped) { $heading = $branch.name $progParams.CurrentOperation = $heading Write-Progress @progParams $div = $branch.name -replace "\W", "" $classGroup = $branch.group | Sort-Object -Property Class, Name | Group-Object -Property Class $fragGroup = foreach ($item in $classGroup) { if ($item.name -eq 'group') { $childDiv = (New-Guid).guid -replace "-", "X" $child = foreach ($groupItem in $item.group) { if ($groupItem.name -notmatch "Domain Users|Domain Computers") { $groupchildDiv = (New-Guid).guid -replace "-", "X" $groupText = $groupitem.Name [xml]$html = Get-ADGroup -Identity $groupitem.distinguishedname -Properties members, WhenChanged -OutVariable g | Select-Object -Property DistinguishedName, GroupScope, GroupCategory, @{Name = "MemberCount"; Expression = { $_.members.count } }, WhenChanged | ConvertTo-Html -Fragment #insert class to set first column width $th = $html.CreateAttribute("class") $th.Value = "dn" [void]($html.table.tr[0].childnodes[0].attributes.append($th)) $groupData = $html.innerxml if ($g.members) { $groupData += "<H5>Members</H5>" $grpUserData = Get-ADGroupUser $groupitem.distinguishedname | Select-Object -Property distinguishedname, name, description, Enabled if (-Not $grpuserdata) { #must be a special group $grpUserData = Get-ADGroupMember -Identity $groupItem.DistinguishedName | Sort-Object DistinguishedName | Select-Object DistinguishedName, Name, SamAccountName, SID, ObjectClass } [xml]$html = $grpUserData | ConvertTo-Html -Fragment #insert class to set first column width if ($html.table) { $th = $html.CreateAttribute("class") $th.Value = "dn" [void]($html.table.tr[0].childnodes[0].attributes.append($th)) } for ($i = 1; $i -le $html.table.tr.count - 1; $i++) { $dn = $html.table.tr[$i].ChildNodes[0]."#text" $pop = $html.CreateAttribute("tip") $pop.value = (_getPopData -Identity $dn | Format-List | Out-String).Trim() [void]($html.table.tr[$i].ChildNodes[0].Attributes.append($pop)) } $groupData += $html.InnerXml } #if g.members _insertToggle -Text $groupText -div $groupchildDiv -Heading "H4" -data $groupData -NoConvert } } #foreach groupitem } #if group else { [xml]$html = $item.group | Select-Object -Property DistinguishedName, Name, Description, Enabled | ConvertTo-Html -Fragment #insert class to set first column width $th = $html.CreateAttribute("class") $th.Value = "dn" [void]($html.table.tr[0].childnodes[0].attributes.append($th)) for ($i = 1; $i -le $html.table.tr.count - 1; $i++) { $dn = $html.table.tr[$i].ChildNodes[0]."#text" $pop = $html.CreateAttribute("tip") $pop.value = (_getPopData -Identity $dn | Format-List | Out-String).Trim() [void]($html.table.tr[$i].ChildNodes[0].Attributes.append($pop)) } $child = $html.InnerXml } _insertToggle -Text "$($item.name)s [$($item.count)]" -div $childDiv -Heading "H3" -data $child -NoConvert } #foreach item in classgroup $fragments.Add($(_inserttoggle -Text $heading -div $div -Heading "H2" -Data $fragGroup -NoConvert)) } #foreach branch #endregion #region create report $cHtml.Body = $Fragments #convert filepath to a valid filesystem name $parent = (Split-Path -Path $filePath) $file = Join-Path -Path $parent -ChildPath (Split-Path -Path $filePath -Leaf) Write-Verbose "[$((Get-Date).TimeofDay)] Saving file to $file" ConvertTo-Html @cHtml | Out-File -FilePath $file #endregion Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)" } #New-ADDomainReport |