Saturday, May 8, 2021
Home Active Directory Active Directory HTML Report

Active Directory HTML Report

This report creates a very nice looking HTML report that can be used directly to report to clients with not much editing. Run on the local server but this time it doesn’t need Office installed to run. Here is a sample of the output.

<#
    .SYNOPSIS
        Creates HTML reports of an active directory forest and its domains.
   
    	THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE 
    	RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.
    	
    	Version 1.7 - 02/13/2014
	
    .DESCRIPTION
        Creates HTML reports of an active directory forest and its domains.

        The following information is reported upon:
        FOREST REPORT
            * Forest Information
                Forest Summary
                - Name/Functional Level
                - Domain/Site/DC/GC/Exchange/Lync/Pool counts
                Forest Features
                - Tombstone Lifetime
                - Recycle Bin Enabled
                - Lync AD Container
                - Lync Version
                - Exchange Version
                Site Summary
                - Site/Subnet/Link/Connection counts
                - Sites without site connections count
                - Sites without ISTG count
                - Sites without subnets count
                - Sites wihtout servers count
                Exchange Servers
                - Organization
                - Administrative Group
                - Name
                - Roles
                - Site
                - Serial
                - Product ID
                Lync Elements
                - Function (Server/Pool)
                - Type (Internal/Edge/Backend/Pool)
                - FQDN
                Registered DHCP Servers
                - Name
                - Creation Date
                Registered NPS Servers
                - Dopmain
                - Name
                - Type
            * Site Information    
                Site Summary
                - Name
                - Location
                - Domains
                - DCs
                - Subnets
                Site Details
                - Name
                - Options
                - ISTG
                - Links
                - Bridgeheads
                - Adjacencies
                Site Subnets
                - Subnet
                - Site Name
                - Location
                Site Connections
                - Enabled
                - Options
                - From
                - To
                Site Links
                - Name
                - Replication Interval
                - Sites
                - Change Notification Enabled
            * Domain Information
                Domains
                - Name
                - NetBIOS
                - Functional Level
                - Forest Root
                - RIDs Issued
                - RIDs Remaining
                Domain Password Policies
                - Name
                - NetBIOS
                - Lockout Threshold
                - Pass History Length
                - Max Pass Age
                - Min Pass Age
                - Min Pass Length
                Domain Controllers
                - Domain
                - Site
                - Name
                - OS
                - Time
                - IP
                - GC
                - FSMO Roles
                Domain Trusts
                - Domain
                - Trusted Domain
                - Direction
                - Attributes
                - Trust Type
                - Created
                - Modified
                Domain DFS Shares
                - Domain
                - Name
                - DN
                - Remote Server
                Domain DFSR Shares
                - Domain
                - Name
                - Content
                - Remote Servers
                Domain Integrated DNS Zones
                - Domain
                - Partition
                - Name
                - Record Count
                - Created
                - Changed
                Domain GPOs
                - Domain
                - Name
                - Created
                - Changed
                Domain Registered Printers
                - Domain
                - Name
                - Server Name
                - Share Name
                - Location
                - Driver Name
                Domain Registered SCCM Servers
                - Domain
                - Name
                - Site Code
                - Version
                - Default MP
                - Device MP
                Domain Registered SCCM Sites
                - Domain
                - Name
                - Site Code
                - Roaming Boundries

            DOMAIN REPORT
            * Domain Statistics
                User Account Statistics 1
                - Total User Accounts
                - Enabled
                - Disabled
                - Locked
                - Password Does Not Expire
                - Password Must Change
                Account Statistics (count) 2
                - Password Not Required
                - Dial-in Enabled
                - Control Access With NPS
                - Unconstrained Delegation
                - Not Trusted For Delegation
                - No Pre-Auth Required
                - Group Statistics
                Total Groups
                - Built-in
                - Universal Security
                - Universal Distribution
                - Global Security
                - Global Distribution
                - Domain Local Security
                - Domain Local Distribution
                Privileged Group Statistics
                - Default Priv Group Name
                - Current Group Name (if it were changed)
                - Member Count
                Privileged Group Membership for the following groups:
                - Enterprise Admins
                - Schema Admins
                - Domain Admins
                - Administrators
                - Cert Publishers
                - Account Operators
                - Server Operators
                - Backup Operators
                - Print Operators
                Account information for the prior groups:
                - Logon ID
                - Name
                - Password Age (Days)
                - Last Logon Date
                - Password Does Not Expire
                - Password Reversable
                - Password Not Required

    	IMPORTANT NOTE: The script requires powershell 3.0 as well as .Net 3.5 for Linq to be 
                        able to highlight HTML cells.
	
	.PARAMETER ReportFormat
        One of three report formats to use; HTML, Excel, and Custom. The first two are precanned options, 
        the last requires custom code further on in the script.
    
        HTML - This is the default option. Saves the report locally.
        Excel - This can be used to spit out all the report elements to excel, each section in its own 
                workbook.
        Custom - You will need to supply your own mix of parameters later in the code to use this.
        
   
    .PARAMETER ReportType
        Which reports will you be generating?
        
        Forest - Generate forest discovery report.
        Domain - Generate per domain privileged user reports.
        ForestAndDomain - Default value. Generate both reports.
    
    .PARAMETER ExportAllUsers
	    When processing the domain information gathering, also export all users with normalized attributes to a CSV.
        
    .PARAMETER ExportPrivilegedUsers
        When processing the domain information gathering, also export all privileged users with normalized attributes to a CSV.
        
    .PARAMETER ExportGraphvizDefinitionFiles
        When processing the forest information gathering, also create export graphviz diagram definition files.
        
    .PARAMETER SaveData
        Save data to an xml file for later report processing.
        
    .PARAMETER LoadData
        Load data for report processing (skips information gathering).
        
    .PARAMETER DataFile
        XML file base name used for domain and forest load/save data (without a path!). This will automatically be prefixed with domain_ or forest_.
        
	.PARAMETER PromptForInput
    	By default global variables are used (which can be found shortly after the parameters section). 
        If PromptForInput is set then the report variables will be prompted for at the console.
    
	.EXAMPLE
        Generate the HTML report using the predefined global variables and preselected html reports. 
        Show verbose status updates (HIGHLY RECOMMENDED!!)
        .\Get-ADAssetReport.ps1 -Verbose
    	
    .EXAMPLE
        Generate the Excel report, prompt for report variables. Be verbose.
        .\Get-ADAssetReport.ps1 -PromptForInput -ReportFormat 'Excel' -Verbose
        
    .EXAMPLE
        Generate the HTML report, prompt for report variables.
        .\Get-ADAssetReport.ps1 -PromptForInput
   
    .EXAMPLE
        Gather forest related information. Create graphviz diagram source files. Save all data collected for later report generation.
        .\Get-ADAssetReport.ps1 -ReportType Forest -ExportGraphvizDefinitionFiles -SaveData

    .EXAMPLE
        Load previously saved xml forest data and generate the HTML report.
        .\Get-ADAssetReport.ps1 -LoadData -ReportType Forest
        
    .NOTES
        Author: Zachary Loeber

        Version History:
        1.7 - 02/13/2014
            - New save/load functionality! With a switch you can export all collected data
              to xml for later report processing.
            - Fixed domain user priveleged report to show lastlogontimestamp as 'never' in html
              report
            - Added change notification attribute to site link report section
            - Small modification to Format-HTMLTable function to catch errors when processing empty tables
            - Slight code clean up
            - Fixed issue with domain report count of passwords set to never expire.
        1.6.1 - 01/15/2014
            - Removed superfluous skipdomainreport and skipforestreport paramenters
            - Swapped out Colorize-Table with Format-HTMLTable. This means pretty HTML
              reports on older systems where the Linq assemblies are not available.
            - Minor fixes.
        1.6 - 01/10/2014
            - Added registered NPS devices
            - Added registered DHCP devices
            - Added domain registered print devices
            - Added SCCM servers and sites
            - Added wrapper parameters to entire script with some most used options for directly
              running the script from a powershell prompt.
            - Added ability to prompt for input for all major global variables.
            - Fixed verbose calling for priv groups and users
            - Updated lastlogontimestamp for user export normalization to show never logged in instead
              of a date from the 1600's.
            - Added date translation for account expiration in account normalization.
            - Updated ad gathering functions to account for inability to connect to domain and silently exit.
            - Slight rearrangement of report sections.
        1.5 - 11/26/2013
            - Added the parameter ForceAnonymous along with the code to force anonymous authentication when 
              sending email reports
        1.4 - 11/21/2013
            - Fixed site connections destiniation server output flaw        
            - Fixed errors occuring when subnets have no sites        
            - Fixed a number of other errors and bugs related to my prior addition of Get-ADPathName.        
            - Fixed issues where phantom domains exist in topology
        1.3 - 11/14/2013
            - Fixed DC count issue
            - Some formatting changes
            - Added detection for newer versions of exchange schemas
            - Changed logic for exchange role detection for 2013 to provide accurate results
            - Fixed linq issues when running on windows 2012 servers
            - Stopped using builtin -split for ldap paths in favor of a custom function called Get-ADPathName
            - Added function for resolving msRTCSIP-PrimaryHomeServer to the user's lync pool name in the CSV 
              export of all users
            - More changes to the base functions (more error handling and such)
        1.2 - 11/10/2013
            - Added site summary section
            - Fixed some code for when no subnets/sites are returned.
            - Fixed site options section (I think)
            - Changed 'AllowEmptyReport' Section element to saner name of 'ShowSectionEvenWithNoData'
            - Commented out write-verbose statements for the report generation portions
            - Added timer in the forest data collection routine (as it was taking way too long to process), found
              pulling all properties in the Search-AD function was a real drag so I manually defined all the properties
              to gather where needed. Should speed things up considerably.
            - Fixed recycle bin detection
            - Prettied up the DC report section to better show FSMO roles and GCs
            - Changed the trusts attribute detection to be an enumeration instead
            - Mild changes to the base report generation functions.
            - Added Exchange Federations section
        1.1 - 11/02/2013
            - Added domain level reporting
            - Added AD Integrated Zone information to forest reports
            - Added GPO information to forest reports
            - Fixed a ton of Powershell V2 related issues
        1.0 - 10/15/2013
            - Initial release of forest level report
        
    .LINK 
        http://www.the-little-things.net 
#>
[CmdletBinding()] 
param ( 
    [Parameter(HelpMessage='Format of report(s) to generate. Defaults to HTML.')]
    [ValidateSet('HTML','Excel','Custom')]
    [String]
    $ReportFormat='HTML',
    
    [Parameter(HelpMessage='Types of report(s) to generate. Defaults to ForestAndDomain.')]
    [ValidateSet('Forest','Domain','ForestAndDomain','Custom')]
    [String]
    $ReportType='ForestAndDomain',
    
    [Parameter(HelpMessage='CSV Export of all users.(Only applies to Domain account report)')]
    [switch]
    $ExportAllUsers,
    
    [Parameter(HelpMessage='CSV Export of all priviledged users. (Only applies to Domain account report)')]
    [switch]
    $ExportPrivilegedUsers,
    
    [Parameter(HelpMessage='Export graphviz definition files for diagram generation.(Only applies to Forest report)')]
    [switch]
    $ExportGraphvizDefinitionFiles,
    
    [Parameter(HelpMessage='Save all gathered data.')]
    [switch]
    $SaveData,
    
    [Parameter(HelpMessage='Load previously saved data.')]
    [switch]
    $LoadData,
    
    [Parameter(HelpMessage='Data file used when saving or loading data.')]
    [String]
    $DataFile='SaveData.xml',
    
    [Parameter(HelpMessage='Prompt for report variables.')]
    [switch]
    $PromptForInput
)

#region Custom Static Variables
# Forest level diagram reports can be enabled here. You can also just enable the source file
# generation for input into dot.exe or the graphviz gui at another workstation.
$AD_CreateDiagramSourceFiles = $ExportGraphvizDefinitionFiles
$AD_CreateDiagrams = $false
$Graphviz_Path = ''

# Added this in as it can be useful to have a list of all users with their
# AD properties sometimes (to massage for input into other scripts among other things)
$EXPORTTOCSV_ALLUSERS = $ExportAllUsers
$EXPORTTOCSV_PRIVUSERS = $ExportPrivilegedUsers

# Used if calling script from command line
$Verbosity = ($PSBoundParameters['Verbose'] -eq $true)

If ($PromptForInput)
{
    $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes",""
    $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No",""
    $choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no)
    
    $result = $Host.UI.PromptForChoice("Create Diagram Source Files?","Do you want to create diagram source txt files for later processing?",$choices,1)
    $AD_CreateDiagramSourceFiles = ($result -ne $true)
    $result = $Host.UI.PromptForChoice("Create Diagrams (requires graphviz binaries)?","Do you want to create diagrams with graphviz?",$choices,1)
    $AD_CreateDiagrams = ($result -ne $true)
    if ($AD_CreateDiagrams)
    {
        $Graphviz_Path = Read-Host "Enter your graphviz binary path if needed (if already in the environment path just press enter):"
    }

    $result = $Host.UI.PromptForChoice("Export All Users?","Do you want to export a CSV of all user data?",$choices,1)
    $EXPORTTOCSV_ALLUSERS = ($result -ne $true)
    $result = $Host.UI.PromptForChoice("Export All Privileged Users?","Do you want to export a CSV of all privileged user data?",$choices,1)
    $EXPORTTOCSV_PRIVUSERS = ($result -ne $true)
    
    $result = $Host.UI.PromptForChoice("Verbose?","Do you want verbose output?",$choices,0)
    $Verbosity = ($result -ne $true)
}

# If you are color coding the domain reports this will control password age colorization
$AD_PwdAgeWarn = 60
$AD_PwdAgeAlert = 90
$AD_PwdAgeHealthy = 60

# A list of user attributes to normalize across all users.
# When an attribute doesn't exist (a non-mailbox enabled
# account for instance), it will be added with a $null value.
# These will all be exported if $EXPORTTOCSV_USERS is $true
$UserAttribs = @(
    'cn',
    'displayName',
    'givenName',
    'sn',
    'name',
    'sAMAccountName',
    'sAMAccountType',
    'whenChanged',
    'whenCreated',
    'pwdLastSet',
    'admincount',
    'accountExpires',
    'badPasswordTime',
    'badPwdCount',
    'lastLogon',
    'lastLogoff',
    'logonCount',
    'useraccountcontrol',
    'lastlogontimestamp',
    'homeMDB',
    'homeMTA',
    'mail',
    'proxyAddresses',
    'mailNickname',
    #'legacyExchangeDN',
    #'showInAddressBook',
    'msexchalobjectversion',
    #'msexchdelegatelistbl',        # Could be interesting for a seperate report
    'msexchhomeservername',
    'msexchrecipientdisplaytype',
    'msexchrecipienttypedetails',
    'msexchumdtmfmap',
    'msexchuseraccountcontrol',
    'msexchuserculture',
    'msexchversion',
    'msexchwhenmailboxcreated',
    'msnpallowdialin',
    'msRTCSIP-PrimaryHomeServer',
    'msRTCSIP-PrimaryUserAddress',
    'msRTCSIP-UserEnabled',
    'msRTCSIP-Line',
    'msRTCSIP-FederationEnabled',
    'msRTCSIP-InternetAccessEnabled'
)

# These are what we will attempt to report upon later on as 'privileged' groups
$AD_PrivilegedGroups = @(
    'Enterprise Admins',
    'Schema Admins',
    'Domain Admins',
    'Administrators',
    'Cert Publishers',
    'Account Operators',
    'Server Operators',
    'Backup Operators',
    'Print Operators'
)

$Attrib_User_MSExchangeVersion = @{
    # $null = Exchange 2003 and earlier
    '4535486012416' = '2007'
    '44220983382016' = '2010'
}

# http://msdn.microsoft.com/en-us/library/cc223546(v=prot.20).aspx
Add-Type -TypeDefinition @" 
    [System.Flags]
    public enum nTDSSiteConnectionSettingsFlags {
        IS_GENERATED                  = 0x00000001,
        TWOWAY_SYNC                   = 0x00000002,
        OVERRIDE_NOTIFY_DEFAULT       = 0x00000004,
        USE_NOTIFY                    = 0x00000008,
        DISABLE_INTERSITE_COMPRESSION = 0x00000010,
        OPT_USER_OWNED_SCHEDULE       = 0x00000020  
    }
    [System.Flags]
    public enum MSExchCurrentServerRolesFlags {
        NONE           = 0x00000001,
        MAILBOX        = 0x00000002,
        CLIENT_ACCESS  = 0x00000004,
        UM             = 0x00000010,
        HUB_TRANSPORT  = 0x00000020,
        EDGE_TRANSPORT = 0x00000040  
    }
    [System.Flags]
    public enum nTDSSiteSettingsFlags {
        IS_AUTO_TOPOLOGY_DISABLED            = 0x00000001,
        IS_TOPL_CLEANUP_DISABLED             = 0x00000002,
        IS_TOPL_MIN_HOPS_DISABLED            = 0x00000004,
        IS_TOPL_DETECT_STALE_DISABLED        = 0x00000008,
        IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED = 0x00000010,
        IS_GROUP_CACHING_ENABLED             = 0x00000020,
        FORCE_KCC_WHISTLER_BEHAVIOR          = 0x00000040,
        FORCE_KCC_W2K_ELECTION               = 0x00000080,
        IS_RAND_BH_SELECTION_DISABLED        = 0x00000100,
        IS_SCHEDULE_HASHING_ENABLED          = 0x00000200,
        IS_REDUNDANT_SERVER_TOPOLOGY_ENABLED = 0x00000400
    }
    [System.Flags]
    public enum MSTrustAttributeFlags {
        NON_TRANSITIVE      = 0x00000001,
        UPLEVEL_ONLY        = 0x00000002,
        QUARANTINED_DOMAIN  = 0x00000004,
        FOREST_TRANSITIVE   = 0x00000008,
        CROSS_ORGANIZATION  = 0x00000010,
        WITHIN_FOREST       = 0x00000020,
        TREAT_AS_EXTERNAL   = 0x00000040,
        USES_RC4_ENCRYPTION = 0x00000080
    }
"@

#Schema constants
$SchemaHashExchange = 
@{
    4397='Exchange Server 2000 RTM'
    4406='Exchange Server 2000 SP3'
    6870='Exchange Server 2003 RTM'
    6936='Exchange Server 2003 SP3'
    10628='Exchange Server 2007 RTM'
    10637='Exchange Server 2007 RTM'
    11116='Exchange 2007 SP1'
    14622='Exchange 2007 SP2 or Exchange 2010 RTM'
    14625='Exchange 2007 SP3'
    14726='Exchange 2010 SP1'
    14732='Exchange 2010 SP2'
    14734='Exchange 2010 SP3'
    15137='Exchange 2013 RTM'
    15254='Exchange 2013 CU1'
    15281='Exchange 2013 CU2'
    15283='Exchange 2013 CU3'
}
$SchemaHashLync = 
@{
    1006="LCS 2005"
    1007="OCS 2007 R1"
    1008="OCS 2007 R2"
    1100="Lync Server 2010"
    1150="Lync Server 2013"
}

# AD DC capabilities list (http://www.ldapexplorer.com/en/manual/103010700-connection-rootdse.htm)
# - Primarily used to determine if a DC is RODC or not (Const LDAP_CAP_ACTIVE_DIRECTORY_PARTIAL_SECRETS_OID = "1.2.840.113556.1.4.1920")
$AD_Capabilities = @{
    '1.2.840.113556.1.4.319' = 'Paged results'
    '1.2.840.113556.1.4.417' = 'Show deleted objects'
    '1.2.840.113556.1.4.473' = 'Sort results'
    '1.2.840.113556.1.4.474' = 'Sort results response'
    '1.2.840.113556.1.4.521' = 'Cross domain move'
    '1.2.840.113556.1.4.528' = 'Server notification'
    '1.2.840.113556.1.4.529' = 'Extended DN'
    '1.2.840.113556.1.4.619' = 'Lazy commit'
    '1.2.840.113556.1.4.800' = 'Active Directory >= Windows 2000'
    '1.2.840.113556.1.4.801' = 'SD flags'
    '1.2.840.113556.1.4.805' = 'Tree delete'
    '1.2.840.113556.1.4.906' = 'Microsoft large integer'
    '1.2.840.113556.1.4.1302' = 'Microsoft OID used with DEN Attributes'
    '1.2.840.113556.1.4.1338' = 'Verify name'
    '1.2.840.113556.1.4.1339' = 'Domain scope'
    '1.2.840.113556.1.4.1340' = 'Search options'
    '1.2.840.113556.1.4.1341' = 'RODC DCPROMO'
    '1.2.840.113556.1.4.1413' = 'Permissive Modify'
    '1.2.840.113556.1.4.1670' = 'Active Directory (v5.1)>= Windows 2003'
    '1.2.840.113556.1.4.1781' = 'Microsoft LDAP fast bind extended request'
    '1.2.840.113556.1.4.1791' = 'NTLM Signing and Sealing'
    '1.2.840.113556.1.4.1851' = 'ADAM / AD LDS Supported'
    '1.2.840.113556.1.4.1852' = 'Quota Control'
    '1.2.840.113556.1.4.1880' = 'ADAM Digest'
   # '1.2.840.113556.1.4.1852' = 'Shutdown Notify'
    '1.2.840.113556.1.4.1920' = 'Partial Secrets'
    '1.2.840.113556.1.4.1935' = 'Active Directory (v6.0) >= Windows 2008'
    '1.2.840.113556.1.4.1947' = 'Force Update'
    '1.2.840.113556.1.4.1948' = 'Range Retrieval No Error'
    '1.2.840.113556.1.4.2026' = 'Input DN'
    '1.2.840.113556.1.4.2064' = 'Show Recycled'
    '1.2.840.113556.1.4.2065' = 'Show Deactivated Link'
    '1.2.840.113556.1.4.2080' = 'Active Directory (v6.1) >= Windows 2008 R2'
}

# Forest Report comments
$Comment_ForestDomainDCs = 
@'
<tr>
<th class="sectioncolumngrouping" colspan=6>Server Information</th>
<th class="sectioncolumngrouping" colspan=6>Roles</th>
</tr>
'@

# Domain Report comments
$Comment_PrivGroup_EnterpriseAdmins = 
@'
A group that exists only at the forest level of domains. The group is authorized to make forest-wide changes in Active Directory, such as adding child domains. By default, the only member of the group is the Administrator account for the forest root domain.
'@
$Comment_PrivGroup_SchemaAdmins =
@'
A group that exists only at the forest level of domains. The group is authorized to make schema changes in Active Directory. By default, the only member of the group is the Administrator account for the forest root domain. No other accounts should be in this group unless schema upgrades are being done.
'@
$Comment_PrivGroup_DomainAdmins =
@'
Members are authorized to administer the domain. By default, the Domain Admins group is a member of the Administrators group on all computers that have joined a domain, including the domain controllers. Domain Admins is the default owner of any object that is created in the domain's Active Directory by any member of the group. If members of the group create other objects, such as files, the default owner is the Administrators group.
'@
$Comment_PrivGroup_Administrators =
@'
After the initial installation of the operating system, the only member of the group is the Administrator account. When a computer joins a domain, the Domain Admins group is added to the Administrators group. When a server becomes a domain controller, the Enterprise Admins group also is added to the Administrators group. The Administrators group has built-in capabilities that give its members full control over the system. The group is the default owner of any object that is created by a member of the group.
'@
$Comment_PrivGroup_AccountOperators =
@'
Exists only on domain controllers. By default, the group has no members. By default, Account Operators have permission to create, modify, and delete accounts for users, groups, and computers in all containers and organizational units (OUs) of Active Directory except the Builtin container and the Domain Controllers OU. Account Operators do not have permission to modify the Administrators and Domain Admins groups, nor do they have permission to modify the accounts for members of those groups.
'@
$Comment_PrivGroup_ServerOperators =
@'
Exists only on domain controllers. By default, the group has no members. Server Operators can log on to a server interactively; create and delete network shares; start and stop services; back up and restore files; format the hard disk of the computer; and shut down the computer.
'@
$Comment_PrivGroup_BackupOperators =
@'
By default, the group has no members. Backup Operators can back up and restore all files on a computer, regardless of the permissions that protect those files. Backup Operators also can log on to the computer and shut it down.
'@
$Comment_PrivGroup_PrintOperators =
@'
Exists only on domain controllers. By default, the only member is the Domain Users group. Print Operators can manage printers and document queues.
'@
$Comment_PrivGroup_CertPublishers =
@'
Exists only on domain controllers. By default, the only member is the Domain Users group. Print Operators can manage printers and document queues.
'@
#endregion Custom Static Variables

#region Global Options and Variables
# Change this to allow for more or less result properties to span horizontally
#  anything equal to or above this threshold will get displayed vertically instead.
#  (NOTE: This only applies to sections set to be dynamic in html reports)
$HorizontalThreshold = 10

$currdir = ''
if ($MyInvocation.MyCommand.Path) {
    $currdir = Split-Path $MyInvocation.MyCommand.Path
} else {
    $currdir = $pwd -replace '^\S+::',''
}
#endregion Global Options and Variables

#region System Report Section Processing Definitions
$ADForestReportPreProcessing =
@'
    Get-ADForestReportInformation @VerboseDebug `
                               -ReportContainer $ReportContainer `
                               -SortedRpts $SortedReports
'@

$ADDomainReportPreProcessing =
@'
    Get-ADDomainReportInformation @VerboseDebug `
                               -ReportContainer $ReportContainer `
                               -SortedRpts $SortedReports
'@

$LyncElements_Postprocessing =
@'
    $temp = Format-HTMLTable $Table -Column 'Type' -ColumnValue 'Internal' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Type' -ColumnValue 'Backend' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Type' -ColumnValue 'Pool' -Attr 'class' -AttrValue 'warn'
            Format-HTMLTable $temp -Column 'Type' -ColumnValue 'Edge' -Attr 'class' -AttrValue 'alert'
'@

$ForestDomainDNSZones_Postprocessing =
@'
    [scriptblock]$scriptblock = {[string]$args[0] -match [string]$args[1]}
    $temp = Format-HTMLTable $Table -Scriptblock $scriptblock -Column 'Name' -ColumnValue 'CNF:' -Attr 'class' -AttrValue 'warn'
            Format-HTMLTable $temp  -Scriptblock $scriptblock -Column 'Name' -ColumnValue 'InProgress' -Attr 'class' -AttrValue 'warn'
'@
$ForestSiteConnections_Postprocessing =
@'
    $temp = Format-HTMLTable $Table -Column 'Enabled' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
            Format-HTMLTable $temp -Column 'Enabled' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
'@

$ForestDomainDCs_Postprocessing =
@'
    $temp = Format-HTMLTable $Table -Column 'GC' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'GC' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
    $temp = Format-HTMLTable $temp -Column 'Infra' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Infra' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
    $temp = Format-HTMLTable $temp -Column 'Naming' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Naming' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
    $temp = Format-HTMLTable $temp -Column 'Schema' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Schema' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
    $temp = Format-HTMLTable $temp -Column 'RID' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'RID' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
    $temp = Format-HTMLTable $temp -Column 'PDC' -ColumnValue 'True' -Attr 'class' -AttrValue 'healthy'
            Format-HTMLTable $temp -Column 'PDC' -ColumnValue 'False' -Attr 'class' -AttrValue 'alert'
'@

$ADPrivUser_Postprocessing =
@'
    [scriptblock]$scriptblock = {[int]$args[0] -ge [int]$args[1]}
    [scriptblock]$scriptblockhealthy = {[int]$args[0] -lt [int]$args[1]}
    $temp = Format-HTMLTable $Table -Column 'No Pwd Expiry' -ColumnValue 'True' -Attr 'class' -AttrValue 'warn'
    $temp = Format-HTMLTable $temp -Column 'No Pwd Expiry' -ColumnValue 'False' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Pwd Reversable' -ColumnValue 'True' -Attr 'class' -AttrValue 'alert'
    $temp = Format-HTMLTable $temp -Column 'Pwd Reversable' -ColumnValue 'False' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Column 'Pwd Not Req.' -ColumnValue 'True' -Attr 'class' -AttrValue 'warn'
    $temp = Format-HTMLTable $temp -Column 'Pwd Not Req.' -ColumnValue 'False' -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Scriptblock $scriptblockhealthy -Column 'Pwd Age (Days)' -ColumnValue $AD_PwdAgeHealthy -Attr 'class' -AttrValue 'healthy'
    $temp = Format-HTMLTable $temp -Scriptblock $scriptblock        -Column 'Pwd Age (Days)' -ColumnValue $AD_PwdAgeWarn -Attr 'class' -AttrValue 'warn'
            Format-HTMLTable $temp -Scriptblock $scriptblock        -Column 'Pwd Age (Days)' -ColumnValue $AD_PwdAgeAlert -Attr 'class' -AttrValue 'alert'
'@
#endregion Report Section Processing Definitions

#region Report Structure Definitions
<#
    Configuration
        TOC - Possibly used in the future to create a table of contents
        PreProcessing - Scriptblock to to information gathering
        SkipSectionBreaks - Allows total bypassing of sections of type 
                            'SectionBreak' in reports
        ReportTypes - List all possible report types. The first one listed
                      here will be the default used if none are specified
                      when generating the report.
        Assets - A list of assets which will be reported upon. These are keys in hashes of data
                 broken down by section. In a self contained asset report this will get
                 populated by the PreProcessing information gathering script. Usually
                 this starts out empty and gets automatically filled.
        PostProcessingEnabled - Usually this is true. Currently postprocessing for my scripts
                                rely heavily on a custom function called Format-HTMLTable which,
                                in turn, relies on at least .Net 3.5 sp2 being available for 
                                Linq assemblies. This is done to try and remove the need for
                                custom modules. If you get a bunch of errors about linq not being
                                available you can simply skip post processing by setting this to
                                be false.
#>
$ADForestReport = @{
    'Configuration' = @{
        'TOC'               = $true
        'PreProcessing'     = $ADForestReportPreProcessing
        'SkipSectionBreaks' = $false
        'ReportTypes'       = @('FullDocumentation','ExcelExport')
        'Assets'            = @()
        'PostProcessingEnabled' = $true
    }
    'Sections' = @{
        'Break_ForestInformation' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 0
            'AllData' = @{}
            'Title' = 'Forest Information'
            'Type' = 'SectionBreak'
            'ReportTypes' = @{
                'ExcelExport' = $false
                'FullDocumentation' = @{
                    'ContainerType' = 'full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' = $true
                }
            }
        }
        'ForestSummary' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 1
            'AllData' = @{}
            'Title' = 'Forest Summary'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Name';e={$_.ForestName}},
                        @{n='Functional Level';e={$_.ForestFunctionalLevel}},
                        @{n='Domain Naming Master';e={$_.DomainNamingMaster}},
                        @{n='Schema Master';e={$_.SchemaMaster}},
                        @{n='Domain Count';e={($_.Domains).Count}},
                        @{n='DC Server Count';e={$_.DomainControllersCount}},
                        @{n='GC Server Count';e={($_.GlobalCatalogs).Count}},
                        @{n='Exchange Server Count';e={$_.ExchangeServerCount}},
                        @{n='Lync Server Count';e={$_.LyncServerCount}},
                        @{n='Lync Pool Count';e={$_.LyncPoolCount}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Name';e={$_.ForestName}},
                        @{n='Functional Level';e={$_.ForestFunctionalLevel}},
                        @{n='Domain Naming Master';e={$_.DomainNamingMaster}},
                        @{n='Schema Master';e={$_.SchemaMaster}},
                        @{n='Domain Count';e={($_.Domains).Count}},
                        @{n='Site Count';e={($_.Sites).Count}},
                        @{n='DC Server Count';e={$_.DomainControllersCount}},
                        @{n='GC Server Count';e={($_.GlobalCatalogs).Count}},
                        @{n='Exchange Server Count';e={$_.ExchangeServerCount}},
                        @{n='Lync Server Count';e={$_.LyncServerCount}},
                        @{n='Lync Pool Count';e={$_.LyncPoolCount}}
                }
            }
        }
        'SiteSummary' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 2
            'AllData' = @{}
            'Title' = 'Site Summary'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Site Count';e={$_.SiteCount}},
                        @{n='Site Subnet Count';e={$_.SiteSubnetCount}},
                        @{n='Site Link Count';e={$_.SiteLinkCount}},
                        @{n='Site Connection Count';e={$_.SiteConnectionCount}},
                        @{n='Sites Without Site Connections';e={$_.SitesWithotuSiteConnections}},
                        @{n='Sites Without ISTG';e={$_.SitesWithoutISTG}},
                        @{n='Sites Without Subnets';e={$_.SitesWithoutSubnets}},
                        @{n='Sites Without Servers';e={$_.SitesWithoutServers}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Site Count';e={$_.SiteCount}},
                        @{n='Site Subnet Count';e={$_.SiteSubnetCount}},
                        @{n='Site Link Count';e={$_.SiteLinkCount}},
                        @{n='Site Connection Count';e={$_.SiteConnectionCount}},
                        @{n='Sites Without Site Connections';e={$_.SitesWithoutSiteConnections}},
                        @{n='Sites Without ISTG';e={$_.SitesWithoutISTG}},
                        @{n='Sites Without Subnets';e={$_.SitesWithoutSubnets}},
                        @{n='Sites Without Servers';e={$_.SitesWithoutServers}}
                }
            }
        }
        'ForestFeatures' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' =3
            'AllData' = @{}
            'Title' = 'Forest Features'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Recycle Bin Enabled';e={$_.RecycleBinEnabled}},
                        @{n='Tombstone Lifetime';e={$_.TombstoneLifetime}},
                        @{n='Exchange Version';e={$_.ExchangeVersion}},
                        @{n='Lync Version';e={$_.LyncVersion}},
                       # @{n='Deleted Object Lifetime';e={$_.DeletedObjectLife}},
                       # @{n='Total Object Backup Lifetime';e={$_.TotalObjectBackupLife}},
                        @{n='Lync AD Container';e={$_.LyncADContainer}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Recycle Bin Enabled';e={$_.RecycleBinEnabled}},
                        @{n='Tombstone Lifetime';e={$_.TombstoneLifetime}},
                        @{n='Exchange Version';e={$_.ExchangeVersion}},
                        @{n='Lync Version';e={$_.LyncVersion}},
                      #  @{n='Deleted Object Lifetime';e={$_.DeletedObjectLife}},
                      #  @{n='Total Object Backup Lifetime';e={$_.TotalObjectBackupLife}},
                        @{n='Lync AD Container';e={$_.LyncADContainer}}
                }
            }
        }
        'ForestLyncInfo' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 4
            'AllData' = @{}
            'Title' = 'Lync Elements'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Function';e={$_.LyncElement}},
                        @{n='Type';e={$_.LyncElementType}},
                        @{n='FQDN';e={$_.LyncElementFQDN}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Function';e={$_.LyncElement}},
                        @{n='Type';e={$_.LyncElementType}},
                        @{n='FQDN';e={$_.LyncElementFQDN}}
                }
            }
            'PostProcessing' = $LyncElements_Postprocessing
        }
        'ForestExchangeInfo' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 5
            'AllData' = @{}
            'Title' = 'Forest Exchange Servers'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Org';e={$_.Organization}},
                        @{n='Admin Group';e={$_.AdminGroup}},
                        @{n='Name';e={$_.Name}},
                        @{n='Roles';e={$_.Role}},
                        @{n='Site';e={$_.Site}},
                        #@{n='Created';e={$_.Created}},
                        @{n='Serial';e={$_.Serial}},
                        @{n='Product ID';e={$_.ProductID}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Org';e={$_.Organization}},
                        @{n='Admin Group';e={$_.AdminGroup}},
                        @{n='Name';e={$_.Name}},
                        @{n='Roles';e={$_.Role}},
                        @{n='Site';e={$_.Site}},
                        #@{n='Created';e={$_.Created}},
                        @{n='Serial';e={$_.Serial}},
                        @{n='Product ID';e={$_.ProductID}}
                }
            }
        }
        'ForestExchangeFederations' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 6
            'AllData' = @{}
            'Title' = 'Forest Exchange Federations'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Org';e={$_.Organization}},
                        @{n='Name';e={$_.Name}},
                        @{n='Enabled';e={$_.Enabled}},
                        @{n='Domains';e={[string]$_.Domains -replace ' ', "`n`r"}},
                        @{n='Allowed Actions';e={[string]$_.AllowedActions -replace ' ', "`n`r"}},
                        @{n='App URI';e={$_.TargetAppURI}},
                        @{n='Autodiscover EPR';e={$_.TargetAutodiscoverEPR}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Org';e={$_.Organization}},
                        @{n='Name';e={$_.Name}},
                        @{n='Enabled';e={$_.Enabled}},
                        @{n='Domains';e={[string]$_.Domains -replace ' ', "<br />`n`r"}},
                        @{n='Allowed Actions';e={[string]$_.AllowedActions -replace ' ', "<br />`n`r"}}
                        #@{n='App URI';e={$_.TargetAppURI}},
                        #@{n='Autodiscover EPR';e={$_.TargetAutodiscoverEPR}}
                }
            }
        }
        'ForestDHCPServers' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 7
            'AllData' = @{}
            'Title' = 'Registered DHCP Servers'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Name}},
                        @{n='Created';e={$_.WhenCreated}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Name}},
                        @{n='Created';e={$_.WhenCreated}}
                }
            }
        }
        'ForestDomainNPSServers' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 8
            'AllData' = @{}
            'Title' = 'Registered NPS Servers'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Type';e={$_.Type}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Type';e={$_.Type}}
                }
            }
        }
        'Break_SiteInformation' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 10
            'AllData' = @{}
            'Title' = 'Site Information'
            'Type' = 'SectionBreak'
            'ReportTypes' = @{
                'ExcelExport' = $false
                'FullDocumentation' = @{
                    'ContainerType' = 'full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' = $true
                }
            }
        }
        'ForestSiteSummary' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 11
            'AllData' = @{}
            'Title' = 'Site Summary'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.SiteName}},
                        @{n='Location';e={$_.Location}},
                        @{n='Domains';e={[string]$_.Domains -replace ' ', "`n`r"}},
                        @{n='DCs';e={[string]$_.Servers -replace ' ', "`n`r"}},
                        @{n='Subnets';e={[string]$_.Subnets  -replace ' ', "`n`r"}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.SiteName}},
                        @{n='Location';e={$_.Location}},
                        @{n='Domains';e={[string]$_.Domains -replace ' ', "<br />`n`r"}},
                        @{n='DCs';e={[string]$_.Servers -replace ' ', "<br />`n`r"}},
                        @{n='Subnets';e={[string]$_.Subnets  -replace ' ', "<br />`n`r"}}
                }
            }
        }
        'ForestSiteDetails' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 12
            'AllData' = @{}
            'Title' = 'Site Details'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.SiteName}},
                        @{n='Options';e={$_.Options}},
                        @{n='ISTG';e={$_.ISTG}},
                        @{n='SiteLinks';e={[string]$_.SiteLinks -replace ' ', "`n`r"}},
                        @{n='BridgeheadServers';e={[string]$_.BridgeheadServers -replace ' ', "`n`r"}},
                        @{n='AdjacentSites';e={[string]$_.AdjacentSites -replace ' ', "`n`r"}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.SiteName}},
                        @{n='Options';e={$_.Options}},
                        @{n='ISTG';e={$_.ISTG}},
                        @{n='SiteLinks';e={[string]$_.SiteLinks -replace ' ', "<br />`n`r"}},
                        @{n='BridgeheadServers';e={[string]$_.BridgeheadServers -replace ' ', "<br />`n`r"}},
                        @{n='AdjacentSites';e={[string]$_.AdjacentSites -replace ' ', "<br />`n`r"}}
                }
            }
        }
        'ForestSiteSubnets' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 13
            'AllData' = @{}
            'Title' = 'Site Subnets'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Subnet';e={$_.Name}},
                        @{n='Site Name';e={$_.SiteName}},
                        @{n='Location';e={$_.Location}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Subnet';e={$_.Name}},
                        @{n='Site Name';e={$_.SiteName}},
                        @{n='Location';e={$_.Location}}
                }
            }
        }
        'ForestSiteConnections' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 14
            'AllData' = @{}
            'Title' = 'Site Connections'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Enabled';e={$_.Enabled}},
                        @{n='Options';e={$_.Options}},
                        @{n='From';e={$_.FromServer}},
                        @{n='To';e={$_.Server}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Enabled';e={$_.Enabled}},
                        @{n='Options';e={$_.Options}},
                        @{n='From';e={$_.FromServer}},
                        @{n='To';e={$_.Server}}
                }
            }
            'PostProcessing' = $ForestSiteConnections_Postprocessing
        }
        'ForestSiteLinks' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 15
            'AllData' = @{}
            'Title' = 'Site Links'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Name}},
                        @{n='Replication Interval';e={$_.repInterval}},
                        @{n='Sites';e={[string]$_.Sites -replace ' ', "`n`r"}},
                        @{n='Change Notification Enabled';e={$_.ChangeNotification}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Name}},
                        @{n='Replication Interval';e={$_.repInterval}},
                        @{n='Sites';e={[string]$_.Sites -replace ' ', "<br />`n`r"}},
                        @{n='Change Notification Enabled';e={$_.ChangeNotification}}
                }
            }
            'PostProcessing' = $False
        }
        'Break_DomainInformation' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 20
            'AllData' = @{}
            'Title' = 'Domain Information'
            'Type' = 'SectionBreak'
            'ReportTypes' = @{
                'ExcelExport' = $false
                'FullDocumentation' = @{
                    'ContainerType' = 'full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' = $true
                }
            }
        }
        'ForestDomains' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 21
            'AllData' = @{}
            'Title' = 'Domains'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Domain}},
                        @{n='NetBIOS';e={$_.NetBIOSName}},
                        @{n='Functional Level';e={$_.DomainFunctionalLevel}},
                        @{n='Forest Root';e={$_.IsForestRoot}},
                        @{n='RIDs Issued';e={$_.RIDsIssued}},
                        @{n='RIDs Remaining';e={$_.RIDsRemaining}},
                        @{n='Naming Master';e={$_.DomainNamingMaster}},
                        @{n='Schema Master';e={$_.SchemaMaster}},
                        @{n='PDC Emulator';e={$_.PDCEmulator}},
                        @{n='RID Master';e={$_.RIDMaster}},
                        @{n='Infra Master';e={$_.InfrastructureMaster}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Domain}},
                        @{n='NetBIOS';e={$_.NetBIOSName}},
                        @{n='Functional Level';e={$_.DomainFunctionalLevel}},
                        @{n='Forest Root';e={$_.IsForestRoot}},
                        @{n='RIDs Issued';e={$_.RIDsIssued}},
                        @{n='RIDs Remaining';e={$_.RIDsRemaining}}
                }
            }
        }
        'ForestDomainPasswordPolicy' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 22
            'AllData' = @{}
            'Title' = 'Domain Password Policies'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Domain}},
                        @{n='NetBIOS';e={$_.NetBIOSName}},
                        @{n='Lockout Threshold';e={$_.lockoutThreshold}},
                        @{n='Password History Length';e={$_.pwdHistoryLength}},
                        @{n='Max Password Age';e={$_.maxPwdAge}},
                        @{n='Min Password Age';e={$_.minPwdAge}},
                        @{n='Min Password Length';e={$_.minPwdLength}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Name';e={$_.Domain}},
                        @{n='NetBIOS';e={$_.NetBIOSName}},
                        @{n='Lockout Threshold';e={$_.lockoutThreshold}},
                        @{n='Password History Length';e={$_.pwdHistoryLength}},
                        @{n='Max Password Age';e={$_.maxPwdAge}},
                        @{n='Min Password Age';e={$_.minPwdAge}},
                        @{n='Min Password Length';e={$_.minPwdLength}}
                }
            }
        }
        'ForestDomainDCs' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 23
            'AllData' = @{}
            'Title' = 'Domain Controllers'
            'Type' = 'Section'
            'Comment' = $Comment_ForestDomainDCs
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Site';e={$_.Site}},
                        @{n='Name';e={$_.Name}},
                        @{n='OS';e={$_.OS}},
                        @{n='Time';e={$_.CurrentTime}},
                        @{n='IP';e={$_.IPAddress}},
                        @{n='GC';e={$_.IsGC}},
                        @{n='Infra';e={$_.IsInfraMaster}},
                        @{n='Naming';e={$_.IsNamingMaster}},
                        @{n='Schema';e={$_.IsSchemaMaster}},
                        @{n='RID';e={$_.IsRidMaster}},
                        @{n='PDC';e={$_.IsPdcMaster}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Site';e={$_.Site}},
                        @{n='Name';e={$_.Name}},
                        @{n='OS';e={$_.OS}},
                        @{n='Time';e={$_.CurrentTime}},
                        @{n='IP';e={$_.IPAddress}},
                        @{n='GC';e={$_.IsGC}},
                        @{n='Infra';e={$_.IsInfraMaster}},
                        @{n='Naming';e={$_.IsNamingMaster}},
                        @{n='Schema';e={$_.IsSchemaMaster}},
                        @{n='RID';e={$_.IsRidMaster}},
                        @{n='PDC';e={$_.IsPdcMaster}}
                }
            }
            'PostProcessing' = $ForestDomainDCs_Postprocessing
        }
        'ForestDomainTrusts' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 24
            'AllData' = @{}
            'Title' = 'Domain Trusts'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Trusted Domain';e={$_.TrustedDomain}},
                        @{n='Direction';e={$_.Direction}},
                        @{n='Attributes';e={$_.Attributes}},
                        @{n='Trust Type';e={$_.TrustType}},
                        @{n='Created';e={$_.Created}},
                        @{n='Modified';e={$_.Modified}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Trusted Domain';e={$_.TrustedDomain}},
                        @{n='Direction';e={$_.Direction}},
                        @{n='Attributes';e={$_.Attributes}},
                        @{n='Trust Type';e={$_.TrustType}},
                        @{n='Created';e={$_.Created}},
                        @{n='Modified';e={$_.Modified}}
                }
            }
        }
        'ForestDomainDFSShares' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 25
            'AllData' = @{}
            'Title' = 'Domain DFS Shares'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='DN';e={$_.DN}},
                        @{n='Remote Server';e={$_.RemoteServerName}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='DN';e={$_.DN}},
                        @{n='Remote Server';e={$_.RemoteServerName}}
                }
            }
        }
        'ForestDomainDFSRShares' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 26
            'AllData' = @{}
            'Title' = 'Domain DFSR Shares'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Content';e={[string]$_.Content -replace ' ', "`n`r"}},
                        @{n='Remote Servers';e={[string]$_.RemoteServerName -replace ' ', "`n`r"}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Content';e={[string]$_.Content -replace ' ', "<br />`n`r"}},
                        @{n='Remote Servers';e={[string]$_.RemoteServerName -replace ' ', "<br />`n`r"}}
                }
            }
        }
        'ForestDomainDNSZones' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 27
            'AllData' = @{}
            'Title' = 'Domain Integrated DNS Zones'
            'Type' = 'Section'
            'Comment' = 'Active Directory integrated DNS zones. Zone names containing CNF: or InProgress may be duplicate and should be reviewed.'
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Partition';e={$_.AppPartition}},
                        @{n='Name';e={$_.Name}},
                        @{n='Record Count';e={$_.RecordCount}},
                        @{n='Created';e={$_.Created}},
                        @{n='Changed';e={$_.Changed}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Partition';e={$_.AppPartition}},
                        @{n='Name';e={$_.Name}},
                        @{n='Record Count';e={$_.RecordCount}},
                        @{n='Created';e={$_.Created}},
                        @{n='Changed';e={$_.Changed}}
                }
            }
            'PostProcessing' = $ForestDomainDNSZones_Postprocessing
        }
        'ForestDomainGPOs' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 28
            'AllData' = @{}
            'Title' = 'Domain GPOs'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Created';e={$_.Created}},
                        @{n='Changed';e={$_.Changed}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Created';e={$_.Created}},
                        @{n='Changed';e={$_.Changed}}
                }
            }
        }
        'ForestDomainPrinters' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 30
            'AllData' = @{}
            'Title' = 'Domain Registered Printers'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='ServerName';e={$_.serverName}},
                        @{n='ShareName';e={$_.printShareName}},
                        @{n='Location';e={$_.location}},
                        @{n='DriverName';e={$_.driverName}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='ServerName';e={$_.serverName}},
                        @{n='ShareName';e={$_.printShareName}},
                        @{n='Location';e={$_.location}},
                        @{n='DriverName';e={$_.driverName}}
                }
            }
        }
        'ForestDomainSCCMServers' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 31
            'AllData' = @{}
            'Title' = 'Registered SCCM Servers'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.dNSHostName}},
                        @{n='Site Code';e={$_.mSSMSSiteCode}},
                        @{n='Version';e={$_.mSSMSVersion}},
                        @{n='Default MP';e={$_.mSSMSDefaultMP}},
                        @{n='Device MP';e={$_.mSSMSDeviceManagementPoint}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.dNSHostName}},
                        @{n='Site Code';e={$_.mSSMSSiteCode}},
                        @{n='Version';e={$_.mSSMSVersion}},
                        @{n='Default MP';e={$_.mSSMSDefaultMP}},
                        @{n='Device MP';e={$_.mSSMSDeviceManagementPoint}}
                }
            }
        }
        'ForestDomainSCCMSites' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 32
            'AllData' = @{}
            'Title' = 'Registered SCCM Sites'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'ExcelExport' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Site Code';e={$_.mSSMSSiteCode}},
                        @{n='Roaming Boundries';e={[string]$_.mSSMSRoamingBoundaries -replace ' ', "`n`r"}}
                }
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Domain';e={$_.Domain}},
                        @{n='Name';e={$_.Name}},
                        @{n='Site Code';e={$_.mSSMSSiteCode}},
                        @{n='Roaming Boundries';e={[string]$_.mSSMSRoamingBoundaries -replace ' ', "<br />`n`r"}}
                }
            }
        }
    }
} 

$ADDomainReport = @{
    'Configuration' = @{
        'TOC'                   = $true
        'PreProcessing'         = $ADDomainReportPreProcessing
        'SkipSectionBreaks'     = $false
        'ReportTypes'           = @('FullDocumentation')
        'Assets'                = @()
        'PostProcessingEnabled' = $true
    }
    'Sections' = @{
        'Break_Stats' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 0
            'AllData' = @{}
            'Title' = 'Domain Statistics'
            'Type' = 'SectionBreak'
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' = $true
                }
            }
        }
        'UserAccountStats1' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 1
            'AllData' = @{}
            'Title' = 'User Account Statistics'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Total User Accounts';e={$_.Total}},
                        @{n='Enabled';e={$_.Enabled}},
                        @{n='Disabled';e={$_.Disabled}},
                        @{n='Locked';e={$_.Locked}},
                        @{n='Password Does Not Expire';e={$_.PwdDoesNotExpire}},
                        @{n='Password Must Change';e={$_.PwdMustChange}}
                }
            }
        }
        'UserAccountStats2' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 2
            'AllData' = @{}
            'Title' = 'User Account Statistics'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Password Not Required';e={$_.PwdNotRequired}},
                        @{n='Dial-in Enabled';e={$_.DialInEnabled}},
                        @{n='Control Access With NPS';e={$_.ControlAccessWithNPS}},
                        @{n='Unconstrained Delegation';e={$_.UnconstrainedDelegation}},
                        @{n='Not Trusted For Delegation';e={$_.NotTrustedForDelegation}},
                        @{n='No Pre-Auth Required';e={$_.NoPreAuthRequired}}
                }
            }
        }
        'GroupStats' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 3
            'AllData' = @{}
            'Title' = 'Group Statistics'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Half'
                    'SectionOverride' = $false
                    'TableType' = 'Vertical'
                    'Properties' =
                        @{n='Total Groups';e={$_.Total}},
                        @{n='Built-in';e={$_.Builtin}},
                        @{n='Universal Security';e={$_.UniversalSecurity}},
                        @{n='Universal Distribution';e={$_.UniversalDist}},
                        @{n='Global Security';e={$_.GlobalSecurity}},
                        @{n='Global Distribution';e={$_.GlobalDist}},
                        @{n='Domain Local Security';e={$_.DomainLocalSecurity}},
                        @{n='Domain Local Distribution';e={$_.DomainLocalDist}}
                }
            }
        }
        'PrivGroupStats' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $true
            'Order' = 10
            'AllData' = @{}
            'Title' = 'Privileged Group Statistics'
            'Type' = 'Section'
            'Comment' = $false
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Default Name';e={$_.AdminGroup}},
                        @{n='Current Name';e={$_.DisplayName}},
                        @{n='Member Count';e={$_.MemberCount}}
                }
            }
        }
        'PrivGroup_EnterpriseAdmins' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 20
            'AllData' = @{}
            'Title' = 'Enterprise Administrators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_EnterpriseAdmins
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_SchemaAdmins' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 21
            'AllData' = @{}
            'Title' = 'Schema Administrators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_SchemaAdmins
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_DomainAdmins' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 22
            'AllData' = @{}
            'Title' = 'Domain Administrators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_DomainAdmins
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_Administrators' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 23
            'AllData' = @{}
            'Title' = 'Administrators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_Administrators
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_ServerOperators' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 24
            'AllData' = @{}
            'Title' = 'Server Operators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_ServerOperators
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_BackupOperators' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 25
            'AllData' = @{}
            'Title' = 'Backup Operators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_BackupOperators
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_AccountOperators' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 26
            'AllData' = @{}
            'Title' = 'Account Operators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_AccountOperators
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_CertPublishers' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 27
            'AllData' = @{}
            'Title' = 'Certificate Publishers'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_CertPublishers
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
        'PrivGroup_PrintOperators' = @{
            'Enabled' = $true
            'ShowSectionEvenWithNoData' = $false
            'Order' = 28
            'AllData' = @{}
            'Title' = 'Print Operators'
            'Type' = 'Section'
            'Comment' = $Comment_PrivGroup_PrintOperators
            'ReportTypes' = @{
                'FullDocumentation' = @{
                    'ContainerType' = 'Full'
                    'SectionOverride' = $false
                    'TableType' = 'Horizontal'
                    'Properties' =
                        @{n='Logon ID';e={$_.sAMAccountName}},
                        @{n='Name';e={$_.name}},
                        @{n='Pwd Age (Days)';e={$_.PasswordAge}},
                        @{n='Last Logged In';e={$_.lastlogontimestamp}},
                        @{n='No Pwd Expiry';e={$_.DONT_EXPIRE_PASSWD}},
                        @{n='Pwd Reversable';e={$_.ENCRYPTED_TEXT_PASSWORD_ALLOWED}},
                        @{n='Pwd Not Req.';e={$_.PASSWD_NOTREQD}}
                }
            }
            'PostProcessing' = $ADPrivUser_Postprocessing
        }
    }
}
#endregion System Report Structure

#region HTML Template Variables
# This is the meat and potatoes of how the reports are spit out. Currently it is
# broken down by html component -> rendering style.
$HTMLRendering = @{
    # Markers: 
    #   <0> - Asset Name
    'Header' = @{
        'DynamicGrid' = @'
<!DOCTYPE html>
<!-- HTML5 Mobile Boilerplate -->
<!--[if IEMobile 7]><html class="no-js iem7"><![endif]-->
<!--[if (gt IEMobile 7)|!(IEMobile)]><!--><html class="no-js" lang="en"><!--<![endif]-->

<!-- HTML5 Boilerplate -->
<!--[if lt IE 7]><html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if (IE 7)&!(IEMobile)]><html class="no-js lt-ie9 lt-ie8" lang="en"><![endif]-->
<!--[if (IE 8)&!(IEMobile)]><html class="no-js lt-ie9" lang="en"><![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"><!--<![endif]-->

<head>

    <meta charset="utf-8">
    <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title><0></title>
    <meta http-equiv="cleartype" content="on">
    <link rel="shortcut icon" href="/favicon.ico">

    <!-- Responsive and mobile friendly stuff -->
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- Stylesheets 
    <link rel="stylesheet" href="css/html5reset.css" media="all">
    <link rel="stylesheet" href="css/responsivegridsystem.css" media="all">
    <link rel="stylesheet" href="css/col.css" media="all">
    <link rel="stylesheet" href="css/2cols.css" media="all">
    <link rel="stylesheet" href="css/3cols.css" media="all">
    -->
    <!--<link rel="stylesheet" href="AllStyles.css" media="all">-->
        <!-- Responsive Stylesheets 
    <link rel="stylesheet" media="only screen and (max-width: 1024px) and (min-width: 769px)" href="/css/1024.css">
    <link rel="stylesheet" media="only screen and (max-width: 768px) and (min-width: 481px)" href="/css/768.css">
    <link rel="stylesheet" media="only screen and (max-width: 480px)" href="/css/480.css">
    -->
    <!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements and feature detects -->
    <!-- <script src="js/modernizr-2.5.3-min.js"></script> -->

    <style type="text/css">
    <!--
        /* html5reset.css - 01/11/2011 */
        html, body, div, span, object, iframe,
        h1, h2, h3, h4, h5, h6, p, blockquote, pre,
        abbr, address, cite, code,
        del, dfn, em, img, ins, kbd, q, samp,
        small, strong, sub, sup, var,
        b, i,
        dl, dt, dd, ol, ul, li,
        fieldset, form, label, legend,
        table, caption, tbody, tfoot, thead, tr, th, td,
        article, aside, canvas, details, figcaption, figure, 
        footer, header, hgroup, menu, nav, section, summary,
        time, mark, audio, video {
            margin: 0;
            padding: 0;
            border: 0;
            outline: 0;
            font-size: 100%;
            vertical-align: baseline;
            background: transparent;
        }
        body {
            line-height: 1;
        }
        article,aside,details,figcaption,figure,
        footer,header,hgroup,menu,nav,section { 
            display: block;
        }
        nav ul {
            list-style: none;
        }
        blockquote, q {
            quotes: none;
        }
        blockquote:before, blockquote:after,
        q:before, q:after {
            content: '';
            content: none;
        }
        a {
            margin: 0;
            padding: 0;
            font-size: 100%;
            vertical-align: baseline;
            background: transparent;
        }
        /* change colours to suit your needs */
        ins {
            background-color: #ff9;
            color: #000;
            text-decoration: none;
        }
        /* change colours to suit your needs */
        mark {
            background-color: #ff9;
            color: #000; 
            font-style: italic;
            font-weight: bold;
        }
        del {
            text-decoration:  line-through;
        }
        abbr[title], dfn[title] {
            border-bottom: 1px dotted;
            cursor: help;
        }
        table {
            border-collapse: collapse;
            border-spacing: 0;
        }
        /* change border colour to suit your needs */
        hr {
            display: block;
            height: 1px;
            border: 0;   
            border-top: 1px solid #cccccc;
            margin: 1em 0;
            padding: 0;
        }
        input, select {
            vertical-align: middle;
        }
        /* RESPONSIVE GRID SYSTEM =============================================================================  */
        /* BASIC PAGE SETUP ============================================================================= */
        body { 
        margin : 0 auto;
        padding : 0;
        font : 100%/1.4 'lucida sans unicode', 'lucida grande', 'Trebuchet MS', verdana, arial, helvetica, helve, sans-serif;     
        color : #000; 
        text-align: center;
        background: #fff url(/images/bodyback.png) left top;
        }
        button, 
        input, 
        select, 
        textarea { 
        font-family : MuseoSlab100, lucida sans unicode, 'lucida grande', 'Trebuchet MS', verdana, arial, helvetica, helve, sans-serif; 
        color : #333; }
        /*  HEADINGS  ============================================================================= */
        h1, h2, h3, h4, h5, h6 {
        font-family:  MuseoSlab300, 'lucida sans unicode', 'lucida grande', 'Trebuchet MS', verdana, arial, helvetica, helve, sans-serif;
        font-weight : normal;
        margin-top: 0px;
        letter-spacing: -1px;
        }
        h1 { 
        font-family:  LeagueGothicRegular, 'lucida sans unicode', 'lucida grande', 'Trebuchet MS', verdana, arial, helvetica, helve, sans-serif;
        color: #000;
        margin-bottom : 0.0em;
        font-size : 4em; /* 40 / 16 */
        line-height : 1.0;
        }
        h2 { 
        color: #222;
        margin-bottom : .5em;
        margin-top : .5em;
        font-size : 2.75em; /* 40 / 16 */
        line-height : 1.2;
        }
        h3 { 
        color: #333;
        margin-bottom : 0.3em;
        letter-spacing: -1px;
        font-size : 1.75em; /* 28 / 16 */
        line-height : 1.3; }
        h4 { 
        color: #444;
        margin-bottom : 0.5em;
        font-size : 1.5em; /* 24 / 16  */
        line-height : 1.25; }
            footer h4 { 
                color: #ccc;
            }
        h5 { 
        color: #555;
        margin-bottom : 1.25em;
        font-size : 1em; /* 20 / 16 */ }
        h6 { 
        color: #666;
        font-size : 1em; /* 16 / 16  */ }
        /*  TYPOGRAPHY  ============================================================================= */
        p, ol, ul, dl, address { 
        margin-bottom : 1.5em; 
        font-size : 1em; /* 16 / 16 = 1 */ }
        p {
        hyphens : auto;  }
        p.introtext {
        font-family:  MuseoSlab100, 'lucida sans unicode', 'lucida grande', 'Trebuchet MS', verdana, arial, helvetica, helve, sans-serif;
        font-size : 2.5em; /* 40 / 16 */
        color: #333;
        line-height: 1.4em;
        letter-spacing: -1px;
        margin-bottom: 0.5em;
        }
        p.handwritten {
        font-family:  HandSean, 'lucida sans unicode', 'lucida grande', 'Trebuchet MS', verdana, arial, helvetica, helve, sans-serif; 
        font-size: 1.375em; /* 24 / 16 */
        line-height: 1.8em;
        margin-bottom: 0.3em;
        color: #666;
        }
        p.center {
        text-align: center;
        }
        .and {
        font-family: GoudyBookletter1911Regular, Georgia, Times New Roman, sans-serif;
        font-size: 1.5em; /* 24 / 16 */
        }
        .heart {
        font-family: Pictos;
        font-size: 1.5em; /* 24 / 16 */
        }
        ul, 
        ol { 
        margin : 0 0 1.5em 0; 
        padding : 0 0 0 24px; }
        li ul, 
        li ol { 
        margin : 0;
        font-size : 1em; /* 16 / 16 = 1 */ }
        dl, 
        dd { 
        margin-bottom : 1.5em; }
        dt { 
        font-weight : normal; }
        b, strong { 
        font-weight : bold; }
        hr { 
        display : block; 
        margin : 1em 0; 
        padding : 0;
        height : 1px; 
        border : 0; 
        border-top : 1px solid #ccc;
        }
        small { 
        font-size : 1em; /* 16 / 16 = 1 */ }
        sub, sup { 
        font-size : 75%; 
        line-height : 0; 
        position : relative; 
        vertical-align : baseline; }
        sup { 
        top : -.5em; }
        sub { 
        bottom : -.25em; }
        .subtext {
            color: #666;
            }
        /* LINKS =============================================================================  */
        a { 
        color : #cc1122;
        -webkit-transition: all 0.3s ease;
        -moz-transition: all 0.3s ease;
        -o-transition: all 0.3s ease;
        transition: all 0.3s ease;
        text-decoration: none;
        }
        a:visited { 
        color : #ee3344; }
        a:focus { 
        outline : thin dotted; 
        color : rgb(0,0,0); }
        a:hover, 
        a:active { 
        outline : 0;
        color : #dd2233;
        }
        footer a { 
        color : #ffffff;
        -webkit-transition: all 0.3s ease;
        -moz-transition: all 0.3s ease;
        -o-transition: all 0.3s ease;
        transition: all 0.3s ease;
        }
        footer a:visited { 
        color : #fff; }
        footer a:focus { 
        outline : thin dotted; 
        color : rgb(0,0,0); }
        footer a:hover, 
        footer a:active { 
        outline : 0;
        color : #fff;
        }
        /* IMAGES ============================================================================= */
        img {
        border : 0;
        max-width: 100%;}
        img.floatleft { float: left; margin: 0 10px 0 0; }
        img.floatright { float: right; margin: 0 0 0 10px; }
        /* TABLES ============================================================================= */
        table { 
        border-collapse : collapse;
        border-spacing : 0;
        margin-bottom : 0em; 
        width : 100%; }
        th, td, caption { 
        padding : .25em 10px .25em 5px; }
        tfoot { 
        font-style : italic; }
        caption { 
        background-color : transparent; }
        /*  MAIN LAYOUT    ============================================================================= */
        #skiptomain { display: none; }
        #wrapper {
            width: 100%;
            position: relative;
            text-align: left;
        }
            #headcontainer {
                width: 100%;
            }
                header {
                    clear: both;
                    width: 100%; /* 1000px / 1250px */
                    font-size: 0.6125em; /* 13 / 16 */
                    max-width: 92.3em; /* 1200px / 13 */
                    margin: 0 auto;
                    padding: 5px 0px 0px 0px;
                    position: relative;
                    color: #000;
                    text-align: center ;
                }
            #maincontentcontainer {
                width: 100%;
            }
                .standardcontainer {
                }
                .darkcontainer {
                    background: rgba(102, 102, 102, 0.05);
                }
                .lightcontainer {
                    background: rgba(255, 255, 255, 0.25);
                }
                    #maincontent{
                        clear: both;
                        width: 80%; /* 1000px / 1250px */
                        font-size: 0.8125em; /* 13 / 16 */
                        max-width: 92.3em; /* 1200px / 13 */
                        margin: 0 auto;
                        padding: 1em 0px;
                        color: #333;
                        line-height: 1.5em;
                        position: relative;
                    }
                    .maincontent{
                        clear: both;
                        width: 80%; /* 1000px / 1250px */
                        font-size: 0.8125em; /* 13 / 16 */
                        max-width: 92.3em; /* 1200px / 13 */
                        margin: 0 auto;
                        padding: 1em 0px;
                        color: #333;
                        line-height: 1.5em;
                        position: relative;
                    }
            #footercontainer {
                width: 100%;    
                border-top: 1px solid #000;
                background: #222 url(/images/footerback.png) left top;
            }
                footer {
                    clear: both;
                    width: 80%; /* 1000px / 1250px */
                    font-size: 0.8125em; /* 13 / 16 */
                    max-width: 92.3em; /* 1200px / 13 */
                    margin: 0 auto;
                    padding: 20px 0px 10px 0px;
                    color: #999;
                }
                footer strong {
                    font-size: 1.077em; /* 14 / 13 */
                    color: #aaa;
                }
                footer a:link, footer a:visited { color: #999; text-decoration: underline; }
                footer a:hover { color: #fff; text-decoration: underline; }
                ul.pagefooterlist, ul.pagefooterlistimages {
                    display: block;
                    float: left;
                    margin: 0px;
                    padding: 0px;
                    list-style: none;
                }
                ul.pagefooterlist li, ul.pagefooterlistimages li {
                    clear: left;
                    margin: 0px;
                    padding: 0px 0px 3px 0px;
                    display: block;
                    line-height: 1.5em;
                    font-weight: normal;
                    background: none;
                }
                ul.pagefooterlistimages li {
                    height: 34px;
                }
                ul.pagefooterlistimages li img {
                    padding: 5px 5px 5px 0px;
                    vertical-align: middle;
                    opacity: 0.75;
                    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)";
                    filter: alpha( opacity  = 75);
                    -webkit-transition: all 0.3s ease;
                    -moz-transition: all 0.3s ease;
                    -o-transition: all 0.3s ease;
                    transition: all 0.3s ease;
                }
                ul.pagefooterlistimages li a
                {
                    text-decoration: none;
                }
                ul.pagefooterlistimages li a:hover img {
                    opacity: 1.0;
                    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
                    filter: alpha( opacity  = 100);
                }
                    #smallprint {
                        margin-top: 20px;
                        line-height: 1.4em;
                        text-align: center;
                        color: #999;
                        font-size: 0.923em; /* 12 / 13 */
                    }
                    #smallprint p{
                        vertical-align: middle;
                    }
                    #smallprint .twitter-follow-button{
                        margin-left: 1em;
                        vertical-align: middle;
                    }
                    #smallprint img {
                        margin: 0px 10px 15px 0px;
                        vertical-align: middle;
                        opacity: 0.5;
                        -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
                        filter: alpha( opacity  = 50);
                        -webkit-transition: all 0.3s ease;
                        -moz-transition: all 0.3s ease;
                        -o-transition: all 0.3s ease;
                        transition: all 0.3s ease;
                    }
                    #smallprint a:hover img {
                        opacity: 1.0;
                        -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
                        filter: alpha( opacity  = 100);
                    }
                    #smallprint a:link, #smallprint a:visited { color: #999; text-decoration: none; }
                    #smallprint a:hover { color: #999; text-decoration: underline; }
        /*  SECTIONS  ============================================================================= */
        .section {
            clear: both;
            padding: 0px;
            margin: 0px;
        }
        /*  CODE  ============================================================================= */
        pre.code {
            padding: 0;
            margin: 0;
            font-family: monospace;
            white-space: pre-wrap;
            font-size: 1.1em;
        }
        strong.code {
            font-weight: normal;
            font-family: monospace;
            font-size: 1.2em;
        }
        /*  EXAMPLE  ============================================================================= */
        #example .col {
            background: #ccc;
            background: rgba(204, 204, 204, 0.85);
        }
        /*  NOTES  ============================================================================= */
        .note {
            position:relative;
            padding:1em 1.5em;
            margin: 0 0 1em 0;
            background: #fff;
            background: rgba(255, 255, 255, 0.5);
            overflow:hidden;
        }
        .note:before {
            content:"";
            position:absolute;
            top:0;
            right:0;
            border-width:0 16px 16px 0;
            border-style:solid;
            border-color:transparent transparent #cccccc #cccccc;
            background:#cccccc;
            -webkit-box-shadow:0 1px 1px rgba(0,0,0,0.3), -1px 1px 1px rgba(0,0,0,0.2);
            -moz-box-shadow:0 1px 1px rgba(0,0,0,0.3), -1px 1px 1px rgba(0,0,0,0.2);
            box-shadow:0 1px 1px rgba(0,0,0,0.3), -1px 1px 1px rgba(0,0,0,0.2);
            display:block; width:0; /* Firefox 3.0 damage limitation */
        }
        .note.rounded {
            -webkit-border-radius:5px 0 5px 5px;
            -moz-border-radius:5px 0 5px 5px;
            border-radius:5px 0 5px 5px;
        }
        .note.rounded:before {
            border-width:8px;
            border-color:#ff #ff transparent transparent;
            background: url(/images/bodyback.png);
            -webkit-border-bottom-left-radius:5px;
            -moz-border-radius:0 0 0 5px;
            border-radius:0 0 0 5px;
        }
        /*  SCREENS  ============================================================================= */
        .siteimage {
            max-width: 90%;
            padding: 5%;
            margin: 0 0 1em 0;
            background: transparent url(/images/stripe-bg.png);
            -webkit-transition: background 0.3s ease;
            -moz-transition: background 0.3s ease;
            -o-transition: background 0.3s ease;
            transition: background 0.3s ease;
        }
        .siteimage:hover {
            background: #bbb url(/images/stripe-bg.png);
            position: relative;
            top: -2px;
            
        }
        /*  COLUMNS  ============================================================================= */
        .twocolumns{
            -moz-column-count: 2;
            -moz-column-gap: 2em;
            -webkit-column-count: 2;
            -webkit-column-gap: 2em;
            column-count: 2;
            column-gap: 2em;
          }
        /*  GLOBAL OBJECTS ============================================================================= */
        .breaker { clear: both; }
        .group:before,
        .group:after {
            content:"";
            display:table;
        }
        .group:after {
            clear:both;
        }
        .group {
            zoom:1; /* For IE 6/7 (trigger hasLayout) */
        }
        .floatleft {
            float: left;
        }
        .floatright {
            float: right;
        }
        /* VENDOR-SPECIFIC ============================================================================= */
        html { 
        -webkit-overflow-scrolling : touch; 
        -webkit-tap-highlight-color : rgb(52,158,219); 
        -webkit-text-size-adjust : 100%; 
        -ms-text-size-adjust : 100%; }
        .clearfix { 
        zoom : 1; }
        ::-webkit-selection { 
        background : rgb(23,119,175); 
        color : rgb(250,250,250); 
        text-shadow : none; }
        ::-moz-selection { 
        background : rgb(23,119,175); 
        color : rgb(250,250,250); 
        text-shadow : none; }
        ::selection { 
        background : rgb(23,119,175); 
        color : rgb(250,250,250); 
        text-shadow : none; }
        button, 
        input[type="button"], 
        input[type="reset"], 
        input[type="submit"] { 
        -webkit-appearance : button; }
        ::-webkit-input-placeholder {
        font-size : .875em; 
        line-height : 1.4; }
        input:-moz-placeholder { 
        font-size : .875em; 
        line-height : 1.4; }
        .ie7 img,
        .iem7 img { 
        -ms-interpolation-mode : bicubic; }
        input[type="checkbox"], 
        input[type="radio"] { 
        box-sizing : border-box; }
        input[type="search"] { 
        -webkit-box-sizing : content-box;
        -moz-box-sizing : content-box; }
        button::-moz-focus-inner, 
        input::-moz-focus-inner { 
        padding : 0;
        border : 0; }
        p {
        /* http://www.w3.org/TR/css3-text/#hyphenation */
        -webkit-hyphens : auto;
        -webkit-hyphenate-character : "\2010";
        -webkit-hyphenate-limit-after : 1;
        -webkit-hyphenate-limit-before : 3;
        -moz-hyphens : auto; }
        /*  SECTIONS  ============================================================================= */
        .section {
            clear: both;
            padding: 0px;
            margin: 0px;
        }
        /*  GROUPING  ============================================================================= */
        .group:before,
        .group:after {
            content:"";
            display:table;
        }
        .group:after {
            clear:both;
        }
        .group {
            zoom:1; /* For IE 6/7 (trigger hasLayout) */
        }
        /*  GRID COLUMN SETUP   ==================================================================== */
        .col {
            display: block;
            float:left;
            margin: 1% 0 1% 1.6%;
        }
        .col:first-child { margin-left: 0; } /* all browsers except IE6 and lower */
        /*  REMOVE MARGINS AS ALL GO FULL WIDTH AT 480 PIXELS */
        @media only screen and (max-width: 480px) {
            .col { 
                margin: 1% 0 1% 0%;
            }
        }
        /*  GRID OF TWO   ============================================================================= */
        .span_2_of_2 {
            width: 100%;
        }
        .span_1_of_2 {
            width: 49.2%;
        }
        /*  GO FULL WIDTH AT LESS THAN 480 PIXELS */
        @media only screen and (max-width: 480px) {
            .span_2_of_2 {
                width: 100%; 
            }
            .span_1_of_2 {
                width: 100%; 
            }
        }
        /*  GRID OF THREE   ============================================================================= */
        .span_3_of_3 {
            width: 100%; 
        }
        .span_2_of_3 {
            width: 66.1%; 
        }
        .span_1_of_3 {
            width: 32.2%; 
        }
        /*  GO FULL WIDTH AT LESS THAN 480 PIXELS */
        @media only screen and (max-width: 480px) {
            .span_3_of_3 {
                width: 100%; 
            }
            .span_2_of_3 {
                width: 100%; 
            }
            .span_1_of_3 {
                width: 100%;
            }
        }
        /*  GRID OF FOUR   ============================================================================= */
        .span_4_of_4 {
            width: 100%; 
        }
        .span_3_of_4 {
            width: 74.6%; 
        }
        .span_2_of_4 {
            width: 49.2%; 
        }
        .span_1_of_4 {
            width: 23.8%; 
        }
        /*  GO FULL WIDTH AT LESS THAN 480 PIXELS */
        @media only screen and (max-width: 480px) {
            .span_4_of_4 {
                width: 100%; 
            }
            .span_3_of_4 {
                width: 100%; 
            }
            .span_2_of_4 {
                width: 100%; 
            }
            .span_1_of_4 {
                width: 100%; 
            }
        }
        
        body {
            font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
        }
        
        table{
            border-collapse: collapse;
            border: none;
            font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
            color: black;
            margin-bottom: 0px;
        }
        table td{
            font-size: 10px;
            padding-left: 0px;
            padding-right: 20px;
            text-align: left;
        }
        table td:last-child{
            padding-right: 5px;
        }
        table th {
            font-size: 12px;
            font-weight: bold;
            padding-left: 0px;
            padding-right: 20px;
            text-align: left;
            border-bottom: 1px  grey solid;
        }
        h2{ 
            clear: both;
            font-size: 200%; 
            margin-left: 20px;
            font-weight: bold;
        }
        h3{
            clear: both;
            font-size: 115%;
            margin-left: 20px;
            margin-top: 30px;
        }
        p{ 
            margin-left: 20px; font-size: 12px;
        }
        table.list{
            float: left;
        }
        table.list td:nth-child(1){
            font-weight: bold;
            border-right: 1px grey solid;
            text-align: right;
        }
        table.list td:nth-child(2){
            padding-left: 7px;
        }
        table tr:nth-child(even) td:nth-child(even){ background: #CCCCCC; }
        table tr:nth-child(odd) td:nth-child(odd){ background: #F2F2F2; }
        table tr:nth-child(even) td:nth-child(odd){ background: #DDDDDD; }
        table tr:nth-child(odd) td:nth-child(even){ background: #E5E5E5; }
        
        /*  Error and warning highlighting - Row*/
        table tr.warn:nth-child(even) td:nth-child(even){ background: #FFFF88; }
        table tr.warn:nth-child(odd) td:nth-child(odd){ background: #FFFFBB; }
        table tr.warn:nth-child(even) td:nth-child(odd){ background: #FFFFAA; }
        table tr.warn:nth-child(odd) td:nth-child(even){ background: #FFFF99; }
        
        table tr.alert:nth-child(even) td:nth-child(even){ background: #FF8888; }
        table tr.alert:nth-child(odd) td:nth-child(odd){ background: #FFBBBB; }
        table tr.alert:nth-child(even) td:nth-child(odd){ background: #FFAAAA; }
        table tr.alert:nth-child(odd) td:nth-child(even){ background: #FF9999; }
        
        table tr.healthy:nth-child(even) td:nth-child(even){ background: #88FF88; }
        table tr.healthy:nth-child(odd) td:nth-child(odd){ background: #BBFFBB; }
        table tr.healthy:nth-child(even) td:nth-child(odd){ background: #AAFFAA; }
        table tr.healthy:nth-child(odd) td:nth-child(even){ background: #99FF99; }
        
        /*  Error and warning highlighting - Cell*/
        table tr:nth-child(even) td.warn:nth-child(even){ background: #FFFF88; }
        table tr:nth-child(odd) td.warn:nth-child(odd){ background: #FFFFBB; }
        table tr:nth-child(even) td.warn:nth-child(odd){ background: #FFFFAA; }
        table tr:nth-child(odd) td.warn:nth-child(even){ background: #FFFF99; }
        
        table tr:nth-child(even) td.alert:nth-child(even){ background: #FF8888; }
        table tr:nth-child(odd) td.alert:nth-child(odd){ background: #FFBBBB; }
        table tr:nth-child(even) td.alert:nth-child(odd){ background: #FFAAAA; }
        table tr:nth-child(odd) td.alert:nth-child(even){ background: #FF9999; }
        
        table tr:nth-child(even) td.healthy:nth-child(even){ background: #88FF88; }
        table tr:nth-child(odd) td.healthy:nth-child(odd){ background: #BBFFBB; }
        table tr:nth-child(even) td.healthy:nth-child(odd){ background: #AAFFAA; }
        table tr:nth-child(odd) td.healthy:nth-child(even){ background: #99FF99; }
        
        /* security highlighting */
        table tr.security:nth-child(even) td:nth-child(even){ 
            border-color: #FF1111; 
            border: 1px #FF1111 solid;
        }
        table tr.security:nth-child(odd) td:nth-child(odd){ 
            border-color: #FF1111; 
            border: 1px #FF1111 solid;
        }
        table tr.security:nth-child(even) td:nth-child(odd){
            border-color: #FF1111; 
            border: 1px #FF1111 solid;
        }
        table tr.security:nth-child(odd) td:nth-child(even){
            border-color: #FF1111; 
            border: 1px #FF1111 solid;
        }
        table th.title{ 
            text-align: center;
            background: #848482;
            border-bottom: 1px  black solid;
            font-weight: bold;
            color: white;
        }
        table th.sectioncomment{ 
            text-align: left;
            background: #848482;
            font-style : italic;
            color: white;
            font-weight: normal;
            
            padding: 0px;
        }
        table th.sectioncolumngrouping{ 
            text-align: center;
            background: #AAAAAA;
            color: black;
            font-weight: bold;
            border:1px solid white;
        }
        table th.sectionbreak{ 
            text-align: center;
            background: #848482;
            border: 2px black solid;
            font-weight: bold;
            color: white;
            font-size: 130%;
        }
        table th.reporttitle{ 
            text-align: center;
            background: #848482;
            border: 2px black solid;
            font-weight: bold;
            color: white;
            font-size: 150%;
        }
        table tr.divide{
            border-bottom: 1px  grey solid;
        }
    -->
    </style></head>

<body>
<div id="wrapper">
'@
        'EmailFriendly' = @'
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Frameset//EN' 'http://www.w3.org/TR/html4/frameset.dtd'>
<html><head><title><0></title>
<style type='text/css'>
<!--
body {
    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
}
table{
   border-collapse: collapse;
   border: none;
   font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
   color: black;
   margin-bottom: 10px;
   margin-left: 20px;
}
table td{
   font-size: 12px;
   padding-left: 0px;
   padding-right: 20px;
   text-align: left;
   border:1px solid black;
}
table th {
   font-size: 12px;
   font-weight: bold;
   padding-left: 0px;
   padding-right: 20px;
   text-align: left;
}

h1{ clear: both;
    font-size: 150%; 
    text-align: center;
  }
h2{ clear: both; font-size: 130%; }

h3{
   clear: both;
   font-size: 115%;
   margin-left: 20px;
   margin-top: 30px;
}

p{ margin-left: 20px; font-size: 12px; }

table.list{ float: left; }
   table.list td:nth-child(1){
   font-weight: bold;
   border: 1px grey solid;
   text-align: right;
}

table th.title{ 
    text-align: center;
    background: #848482;
    border: 2px  grey solid;
    font-weight: bold;
    color: white;
}
table tr.divide{
    border-bottom: 5px  grey solid;
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
.warn { background-color:yellow; }
.alert { background-color:red; }
-->
</style>
</head>
<body>
'@
    }
    'Footer' = @{
        'DynamicGrid' = @'
</div>
</body>
</html>        
'@
        'EmailFriendly' = @'
</div>
</body>
</html>       
'@
    }

    # Markers: 
    #   <0> - Server Name
    'ServerBegin' = @{
        'DynamicGrid' = @'
    
    <hr noshade size="3" width='100%'>
    <div id="headcontainer">
        <table>        
            <tr>
                <th class="reporttitle"><0></th>
            </tr>
        </table>
    </div>
    <div id="maincontentcontainer">
        <div id="maincontent">
            <div class="section group">
                <hr noshade size="3" width='100%'>
            </div>
            <div>

       
'@
        'EmailFriendly' = @'
    <div id='report'>
    <hr noshade size=3 width='100%'>
    <h1><0></h1>

    <div id="maincontentcontainer">
    <div id="maincontent">
      <div class="section group">
        <hr noshade="noshade" size="3" width="100%" style=
        "display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;" />
      </div>
      <div>

'@    
    }
    'ServerEnd' = @{
        'DynamicGrid' = @'

            </div>
        </div>
    </div>
</div>

'@
        'EmailFriendly' = @'

            </div>
        </div>
    </div>
</div>

'@
    }
    
    # Markers: 
    #   <0> - columns to span title
    #   <1> - Table header title
    'TableTitle' = @{
        'DynamicGrid' = @'
        
            <tr>
                <th class="title" colspan=<0>><1></th>
            </tr>
'@
        'EmailFriendly' = @'
            
            <tr>
              <th class="title" colspan="<0>"><1></th>
            </tr>
              
'@
    }
    
    'TableComment' = @{
        'DynamicGrid' = @'
        
            <tr>
                <th class="sectioncomment" colspan=<0>><1></th>
            </tr>
'@
        'EmailFriendly' = @'
            
            <tr>
              <th class="sectioncomment" colspan="<0>"><1></th>
            </tr>
              
'@
    }    

    'SectionContainers' = @{
        'DynamicGrid'  = @{
            'Half' = @{
                'Head' = @'
        
        <div class="col span_2_of_4">
'@
                'Tail' = @'
        </div>
'@
            }
            'Full' = @{
                'Head' = @'
        
        <div class="col span_4_of_4">
'@
                'Tail' = @'
        </div>
'@
            }
            'Third' = @{
                'Head' = @'
        
        <div class="col span_1_of_3">
'@
                'Tail' = @'
        </div>
'@
            }
            'TwoThirds' = @{
                'Head' = @'
        
        <div class="col span_2_of_3">
'@
                'Tail' = @'
                
        </div>
'@
            }
            'Fourth'        = @{
                'Head' = @'
        
        <div class="col span_1_of_4">
'@
                'Tail' = @'
                
        </div>
'@
            }
            'ThreeFourths'  = @{
                'Head' = @'
               
        <div class="col span_3_of_4">
'@
                'Tail'          = @'
        
        </div>
'@
            }
        }
        'EmailFriendly'  = @{
            'Half' = @{
                'Head' = @'
        
        <div class="col span_2_of_4">
        <table><tr WIDTH="50%">
'@
                'Tail' = @'
        </tr></table>       
        </div>
'@
            }
            'Full' = @{
                'Head' = @'
        
        <div class="col span_4_of_4">
'@
                'Tail' = @'
                
        </div>
'@
            }
            'Third' = @{
                'Head' = @'
        
        <div class="col span_1_of_3">
'@
                'Tail' = @'
                
        </div>
'@
            }
            'TwoThirds' = @{
                'Head' = @'
        
        <div class="col span_2_of_3">
'@
                'Tail' = @'
                
        </div>
'@
            }
            'Fourth'        = @{
                'Head' = @'
        
        <div class="col span_1_of_4">
'@
                'Tail' = @'
                
        </div>
'@
            }
            'ThreeFourths'  = @{
                'Head' = @'
               
        <div class="col span_3_of_4">
'@
                'Tail'          = @'
        
        </div>
'@
            }
        }
    }
    
    'SectionContainerGroup' = @{
        'DynamicGrid' = @{ 
            'Head' = @'
        
        <div class="section group">
'@
            'Tail' = @'
        </div>
'@
        }
        'EmailFriendly' = @{
            'Head' = @'
    
        <div class="section group">
'@
            'Tail' = @'
        </div>
'@
        }
    }
    
    'CustomSections' = @{
        # Markers: 
        #   <0> - Header
        'SectionBreak' = @'
    
    <div class="section group">        
        <div class="col span_4_of_4"><table>        
            <tr>
                <th class="sectionbreak"><0></th>
            </tr>
        </table>
        </div>
    </div>
'@
    }
}
#endregion HTML Template Variables
#endregion Globals

#region Functions - Serial or Utility
Function ConvertTo-PropertyValue 
{
    <#
    .SYNOPSIS
    Convert an object with various properties into an array of property, value pairs 
    
    .DESCRIPTION
    Convert an object with various properties into an array of property, value pairs

    If you output reports or other formats where a table with one long row is poorly formatted, this is a quick way to create a table of property value pairs.

    There are other ways you could do this.  For example, I could list all noteproperties from Get-Member results and return them.
    This function will keep properties in the same order they are provided, which can often be helpful for readability of results.

    .PARAMETER inputObject
    A single object to convert to an array of property value pairs.

    .PARAMETER leftheader
    Header for the left column.  Default:  Property

    .PARAMETER rightHeader
    Header for the right column.  Default:  Value

    .PARAMETER memberType
    Return only object members of this membertype.  Default:  Property, NoteProperty, ScriptProperty

    .EXAMPLE
    get-process powershell_ise | convertto-propertyvalue

    I want details on the powershell_ise process.
        With this command, if I output this to a table, a csv, etc. I will get a nice vertical listing of properties and their values
        Without this command, I get a long row with the same info

    .EXAMPLE
    #This example requires and demonstrates using the New-HTMLHead, New-HTMLTable, Add-HTMLTableColor, ConvertTo-PropertyValue and Close-HTML functions.
    
    #get processes to work with
        $processes = Get-Process
    
    #Build HTML header
        $HTML = New-HTMLHead -title "Process details"

    #Add CPU time section with top 10 PrivateMemorySize processes.  This example does not highlight any particular cells
        $HTML += "<h3>Process Private Memory Size</h3>"
        $HTML += New-HTMLTable -inputObject $($processes | sort PrivateMemorySize -Descending | select name, PrivateMemorySize -first 10)

    #Add Handles section with top 10 Handle usage.
    $handleHTML = New-HTMLTable -inputObject $($processes | sort handles -descending | select Name, Handles -first 10)

        #Add highlighted colors for Handle count
            
            #build hash table with parameters for Add-HTMLTableColor.  Argument and AttrValue will be modified each time we run this.
            $params = @{
                Column = "Handles" #I'm looking for cells in the Handles column
                ScriptBlock = {[double]$args[0] -gt [double]$args[1]} #I want to highlight if the cell (args 0) is greater than the argument parameter (arg 1)
                Attr = "Style" #This is the default, don't need to actually specify it here
            }

            #Add yellow, orange and red shading
            $handleHTML = Add-HTMLTableColor -HTML $handleHTML -Argument 1500 -attrValue "background-color:#FFFF99;" @params
            $handleHTML = Add-HTMLTableColor -HTML $handleHTML -Argument 2000 -attrValue "background-color:#FFCC66;" @params
            $handleHTML = Add-HTMLTableColor -HTML $handleHTML -Argument 3000 -attrValue "background-color:#FFCC99;" @params
      
        #Add title and table
        $HTML += "<h3>Process Handles</h3>"
        $HTML += $handleHTML

    #Add process list containing first 10 processes listed by get-process.  This example does not highlight any particular cells
        $HTML += New-HTMLTable -inputObject $($processes | select name -first 10 ) -listTableHead "Random Process Names"

    #Add property value table showing details for PowerShell ISE
        $HTML += "<h3>PowerShell Process Details PropertyValue table</h3>"
        $processDetails = Get-process powershell_ise | select name, id, cpu, handles, workingset, PrivateMemorySize, Path -first 1
        $HTML += New-HTMLTable -inputObject $(ConvertTo-PropertyValue -inputObject $processDetails)

    #Add same PowerShell ISE details but not in property value form.  Close the HTML
        $HTML += "<h3>PowerShell Process Details object</h3>"
        $HTML += New-HTMLTable -inputObject $processDetails | Close-HTML

    #write the HTML to a file and open it up for viewing
        set-content C:\test.htm $HTML
        & 'C:\Program Files\Internet Explorer\iexplore.exe' C:\test.htm

    .FUNCTIONALITY
    General Command
    #> 
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true)]
        [PSObject]$InputObject,
        
        [validateset("AliasProperty", "CodeProperty", "Property", "NoteProperty", "ScriptProperty",
            "Properties", "PropertySet", "Method", "CodeMethod", "ScriptMethod", "Methods",
            "ParameterizedProperty", "MemberSet", "Event", "Dynamic", "All")]
        [string[]]$memberType = @( "NoteProperty", "Property", "ScriptProperty" ),
            
        [string]$leftHeader = "Property",
            
        [string]$rightHeader = "Value"
    )

    begin{
        #init array to dump all objects into
        $allObjects = @()

    }
    process{
        #if we're taking from pipeline and get more than one object, this will build up an array
        $allObjects += $inputObject
    }

    end{
        #use only the first object provided
        $allObjects = $allObjects[0]

        #Get properties.  Filter by memberType.
        $properties = $allObjects.psobject.properties | ?{$memberType -contains $_.memberType} | select -ExpandProperty Name

        #loop through properties and display property value pairs
        foreach($property in $properties){

            #Create object with property and value
            $temp = "" | select $leftHeader, $rightHeader
            $temp.$leftHeader = $property.replace('"',"")
            $temp.$rightHeader = try { $allObjects | select -ExpandProperty $temp.$leftHeader -erroraction SilentlyContinue } catch { $null }
            $temp
        }
    }
}

Function ConvertTo-HashArray
{
    <#
    .SYNOPSIS
    Convert an array of objects to a hash table based on a single property of the array. 
    
    .DESCRIPTION
    Convert an array of objects to a hash table based on a single property of the array.
    
    .PARAMETER InputObject
    An array of objects to convert to a hash table array.

    .PARAMETER PivotProperty
    The property to use as the key value in the resulting hash.
    
    .PARAMETER LookupValue
    Property in the psobject to be the value that the hash key points to in the returned result. If not specified, all properties in the psobject are used.

    .EXAMPLE
    $DellServerHealth = @(Get-DellServerhealth @_dellhardwaresplat)
    $DellServerHealth = ConvertTo-HashArray $DellServerHealth 'PSComputerName'

    Description
    -----------
    Calls a function which returns a psobject then converts that result to a hash array based on the PSComputerName
    
    .NOTES
    Author:
    Zachary Loeber
    
    Version Info:
    1.1 - 11/17/2013
        - Added LookupValue Parameter to allow for creation of one to one hashs
        - Added more error validation
        - Dolled up the paramerters
        
    .LINK 
    http://www.the-little-things.net 
    #> 
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   HelpMessage='A single or array of PSObjects',
                   Position=0)]
        [AllowEmptyCollection()]
        [PSObject[]]
        $InputObject,
        
        [Parameter(Mandatory=$true,
                   HelpMessage='Property in the psobject to be the future key in a returned hash.',
                   Position=1)]
        [string]$PivotProperty,
        
        [Parameter(HelpMessage='Property in the psobject to be the value that the hash key points to. If not specified, all properties in the psobject are used.',
                   Position=2)]
        [string]$LookupValue = ''
    )

    BEGIN
    {
        #init array to dump all objects into
        $allObjects = @()
        $Results = @{}
    }
    PROCESS
    {
        #if we're taking from pipeline and get more than one object, this will build up an array
        $allObjects += $inputObject
    }

    END
    {
        ForEach ($object in $allObjects)
        {
            if ($object -ne $null)
            {
                try
                {
                    if ($object.PSObject.Properties.Match($PivotProperty).Count) 
                    {
                        if ($LookupValue -eq '')
                        {
                            $Results[$object.$PivotProperty] = $object
                        }
                        else
                        {
                            if ($object.PSObject.Properties.Match($LookupValue).Count)
                            {
                                $Results[$object.$PivotProperty] = $object.$LookupValue
                            }
                            else
                            {
                                Write-Warning -Message ('ConvertTo-HashArray: LookupValue Not Found - {0}' -f $_.Exception.Message)
                            }
                        }
                    }
                    else
                    {
                        Write-Warning -Message ('ConvertTo-HashArray: LookupValue Not Found - {0}' -f $_.Exception.Message)
                    }
                }
                catch
                {
                    Write-Warning -Message ('ConvertTo-HashArray: Something weird happened! - {0}' -f $_.Exception.Message)
                }
            }
        }
        $Results
    }
}

Function ConvertTo-PSObject
{
    <# 
     Take an array of like psobject and convert it to a singular psobject based on two shared
     properties across all psobjects in the array.
     Example Input object: 
    $obj = @()
    $a = @{ 
        'PropName' = 'Property 1'
        'Val1' = 'Value 1'
        }
    $b = @{ 
        'PropName' = 'Property 2'
        'Val1' = 'Value 2'
        }
    $obj += new-object psobject -property $a
    $obj += new-object psobject -property $b

    $c = $obj | ConvertTo-PSObject -propname 'PropName' -valname 'Val1'
    $c.'Property 1'
    Value 1
    #>
    [cmdletbinding()]
    PARAM(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true)]
        [PSObject[]]$InputObject,
        [string]$propname,
        [string]$valname
    )

    BEGIN
    {
        #init array to dump all objects into
        $allObjects = @()
    }
    PROCESS
    {
        #if we're taking from pipeline and get more than one object, this will build up an array
        $allObjects += $inputObject
    }
    END
    {
        $returnobject = New-Object psobject
        foreach ($obj in $allObjects)
        {
            if ($obj.$propname -ne $null)
            {
                $returnobject | Add-Member -MemberType NoteProperty -Name $obj.$propname -Value $obj.$valname
            }
        }
        $returnobject
    }
}

Function ConvertTo-MultiArray 
{
 <#
 .Notes
 NAME: ConvertTo-MultiArray
 AUTHOR: Tome Tanasovski
 Website: http://powertoe.wordpress.com
 Twitter: http://twitter.com/toenuff
 Version: 1.0
 CREATED: 11/5/2010
 LASTEDIT:
 11/5/2010 1.0
 Initial Release
 11/5/2010 1.1
 Removed array parameter and passes a reference to the multi-dimensional array as output to the cmdlet
 11/5/2010 1.2
 Modified all rows to ensure they are entered as string values including $null values as a blank ("") string.

 .Synopsis
 Converts a collection of PowerShell objects into a multi-dimensional array

 .Description
 Converts a collection of PowerShell objects into a multi-dimensional array.  The first row of the array contains the property names.  Each additional row contains the values for each object.

 This cmdlet was created to act as an intermediary to importing PowerShell objects into a range of cells in Exchange.  By using a multi-dimensional array you can greatly speed up the process of adding data to Excel through the Excel COM objects.

 .Parameter InputObject
 Specifies the objects to export into the multi dimensional array.  Enter a variable that contains the objects or type a command or expression that gets the objects. You can also pipe objects to ConvertTo-MultiArray.

 .Inputs
 System.Management.Automation.PSObject
        You can pipe any .NET Framework object to ConvertTo-MultiArray

 .Outputs
 [ref]
        The cmdlet will return a reference to the multi-dimensional array.  To access the array itself you will need to use the Value property of the reference

 .Example
 $arrayref = get-process |Convertto-MultiArray

 .Example
 $dir = Get-ChildItem c:\
 $arrayref = Convertto-MultiArray -InputObject $dir

 .Example
 $range.value2 = (ConvertTo-MultiArray (get-process)).value

 .LINK

http://powertoe.wordpress.com

#>
    param(
        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
        [PSObject[]]$InputObject
    )
    BEGIN {
        $objects = @()
        [ref]$array = [ref]$null
    }
    Process {
        $objects += $InputObject
    }
    END {
        $properties = $objects[0].psobject.properties |%{$_.name}
        $array.Value = New-Object 'object[,]' ($objects.Count+1),$properties.count
        # i = row and j = column
        $j = 0
        $properties |%{
            $array.Value[0,$j] = $_.tostring()
            $j++
        }
        $i = 1
        $objects |% {
            $item = $_
            $j = 0
            $properties | % {
                if ($item.($_) -eq $null) {
                    $array.value[$i,$j] = ""
                }
                else {
                    $array.value[$i,$j] = $item.($_).tostring()
                }
                $j++
            }
            $i++
        }
        $array
    }
}

Function Format-HTMLTable 
{
    <# 
    .SYNOPSIS 
        Format-HTMLTable - Selectively color elements of of an html table based on column value or even/odd rows.
     
    .DESCRIPTION 
        Create an html table and colorize individual cells or rows of an array of objects 
        based on row header and value. Optionally, you can also modify an existing html 
        document or change only the styles of even or odd rows.
     
    .PARAMETER InputObject 
        An array of objects (ie. (Get-process | select Name,Company) 
     
    .PARAMETER  Column 
        The column you want to modify. (Note: If the parameter ColorizeMethod is not set to ByValue the 
        Column parameter is ignored)

    .PARAMETER ScriptBlock
        Used to perform custom cell evaluations such as -gt -lt or anything else you need to check for in a
        table cell element. The scriptblock must return either $true or $false and is, by default, just
        a basic -eq comparisson. You must use the variables as they are used in the following example.
        (Note: If the parameter ColorizeMethod is not set to ByValue the ScriptBlock parameter is ignored)

        [scriptblock]$scriptblock = {[int]$args[0] -gt [int]$args[1]}

        $args[0] will be the cell value in the table
        $args[1] will be the value to compare it to

        Strong typesetting is encouraged for accuracy.

    .PARAMETER  ColumnValue 
        The column value you will modify if ScriptBlock returns a true result. (Note: If the parameter 
        ColorizeMethod is not set to ByValue the ColumnValue parameter is ignored).
     
    .PARAMETER  Attr 
        The attribute to change should ColumnValue be found in the Column specified. 
        - A good example is using "style" 

    .PARAMETER  AttrValue 
        The attribute value to set when the ColumnValue is found in the Column specified 
        - A good example is using "background: red;" 
    
    .PARAMETER DontUseLinq
        Use inline C# Linq calls for html table manipulation by default. This is extremely fast but requires .NET 3.5 or above.
        Use this switch to force using non-Linq method (xml) first.
        
    .PARAMETER Fragment
        Return only the HTML table instead of a full document.
    
    .EXAMPLE 
        This will highlight the process name of Dropbox with a red background. 

        $TableStyle = @'
        <title>Process Report</title> 
            <style>             
            BODY{font-family: Arial; font-size: 8pt;} 
            H1{font-size: 16px;} 
            H2{font-size: 14px;} 
            H3{font-size: 12px;} 
            TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;} 
            TH{border: 1px solid black; background: #dddddd; padding: 5px; color: #000000;} 
            TD{border: 1px solid black; padding: 5px;} 
            </style>
        '@

        $tabletocolorize = Get-Process | Select Name,CPU,Handles | ConvertTo-Html -Head $TableStyle
        $colorizedtable = Format-HTMLTable $tabletocolorize -Column "Name" -ColumnValue "Dropbox" -Attr "style" -AttrValue "background: red;" -HTMLHead $TableStyle
        $colorizedtable = Format-HTMLTable $colorizedtable -Attr "style" -AttrValue "background: grey;" -ColorizeMethod 'ByOddRows' -WholeRow:$true
        $colorizedtable = Format-HTMLTable $colorizedtable -Attr "style" -AttrValue "background: yellow;" -ColorizeMethod 'ByEvenRows' -WholeRow:$true
        $colorizedtable | Out-File "$pwd/testreport.html" 
        ii "$pwd/testreport.html"

    .EXAMPLE 
        Using the same $TableStyle variable above this will create a table of top 5 processes by memory usage,
        color the background of a whole row yellow for any process using over 150Mb and red if over 400Mb.

        $tabletocolorize = $(get-process | select -Property ProcessName,Company,@{Name="Memory";Expression={[math]::truncate($_.WS/ 1Mb)}} | Sort-Object Memory -Descending | Select -First 5 ) 

        [scriptblock]$scriptblock = {[int]$args[0] -gt [int]$args[1]}
        $testreport = Format-HTMLTable $tabletocolorize -Column "Memory" -ColumnValue 150 -Attr "style" -AttrValue "background:yellow;" -ScriptBlock $ScriptBlock -HTMLHead $TableStyle -WholeRow $true
        $testreport = Format-HTMLTable $testreport -Column "Memory" -ColumnValue 400 -Attr "style" -AttrValue "background:red;" -ScriptBlock $ScriptBlock -WholeRow $true
        $testreport | Out-File "$pwd/testreport.html" 
        ii "$pwd/testreport.html"

    .NOTES 
        If you are going to convert something to html with convertto-html in powershell v2 there is 
        a bug where the header will show up as an asterick if you only are converting one object property. 

        This script is a modification of something I found by some rockstar named Jaykul at this site
        http://stackoverflow.com/questions/4559233/technique-for-selectively-formatting-data-in-a-powershell-pipeline-and-output-as

        .Net 3.5 or above is a requirement for using the Linq libraries.

    Version Info:
    1.2 - 01/12/2014
        - Changed bool parameters to switch
        - Added DontUseLinq parameter
        - Changed function name to be less goofy sounding
        - Updated the add-type custom namespace from Huddled to CustomLinq
        - Added help messages to fuction parameters.
        - Added xml method for function to use if the linq assemblies couldn't be loaded (slower but still works)
    1.1 - 11/13/2013
        - Removed the explicit definition of Csharp3 in the add-type definition to allow windows 2012 compatibility.
        - Fixed up parameters to remove assumed values
        - Added try/catch around add-type to detect and prevent errors when processing on systems which do not support
          the linq assemblies.
    .LINK 
        http://www.the-little-things.net 
    #> 
    [CmdletBinding( DefaultParameterSetName = "StringSet")] 
    param ( 
        [Parameter( Position=0,
                    Mandatory=$true, 
                    ValueFromPipeline=$true, 
                    ParameterSetName="ObjectSet",
                    HelpMessage="Array of psobjects to convert to an html table and modify.")]
        [Object[]]
        $InputObject,
        
        [Parameter( Position=0, 
                    Mandatory=$true, 
                    ValueFromPipeline=$true, 
                    ParameterSetName="StringSet",
                    HelpMessage="HTML table to modify.")] 
        [string]
        $InputString='',
        
        [Parameter( HelpMessage="Column name to compare values against when updating the table by value.")]
        [string]
        $Column="Name",
        
        [Parameter( HelpMessage="Value to compare when updating the table by value.")]
        $ColumnValue=0,
        
        [Parameter( HelpMessage="Custom script block for table conditions to search for when updating the table by value.")]
        [scriptblock]
        $ScriptBlock = {[string]$args[0] -eq [string]$args[1]}, 
        
        [Parameter( Mandatory=$true,
                    HelpMessage="Attribute to append to table element.")] 
        [string]
        $Attr,
        
        [Parameter( Mandatory=$true,
                    HelpMessage="Value to assign to attribute.")] 
        [string]
        $AttrValue,
        
        [Parameter( HelpMessage="By default the td element (individual table cell) is modified. This switch causes the attributes for the entire row (tr) to update instead.")] 
        [switch]
        $WholeRow,
        
        [Parameter( HelpMessage="If an array of object is converted to html prior to modification this is the head data which will get prepended to it.")]
        [string]
        $HTMLHead='<title>HTML Table</title>',
        
        [Parameter( HelpMessage="Method for table modification. ByValue uses column name lookups. ByEvenRows/ByOddRows are exactly as they sound.")]
        [ValidateSet('ByValue','ByEvenRows','ByOddRows')]
        [string]
        $ColorizeMethod='ByValue',
        
        [Parameter( HelpMessage="Use inline C# Linq calls for html table manipulation by default. Extremely fast but requires .NET 3.5 or above to work. Use this switch to force using non-Linq method (xml) first.")] 
        [switch]
        $DontUseLinq,
        
        [Parameter( HelpMessage="Return only the html table element.")] 
        [switch]
        $Fragment
        )
    
    BEGIN 
    {
        $LinqAssemblyLoaded = $false
        if (-not $DontUseLinq)
        {
            # A little note on Add-Type, this adds in the assemblies for linq with some custom code. The first time this 
            # is run in your powershell session it is compiled and loaded into your session. If you run it again in the same
            # session and the code was not changed at all, powershell skips the command (otherwise recompiling code each time
            # the function is called in a session would be pretty ineffective so this is by design). If you make any changes
            # to the code, even changing one space or tab, it is detected as new code and will try to reload the same namespace
            # which is not allowed and will cause an error. So if you are debugging this or changing it up, either change the
            # namespace as well or exit and restart your powershell session.
            #
            # And some notes on the actual code. It is my first jump into linq (or C# for that matter) so if it looks not so 
            # elegant or there is a better way to do this I'm all ears. I define four methods which names are self-explanitory:
            # - GetElementByIndex
            # - GetElementByValue
            # - GetOddElements
            # - GetEvenElements
            $LinqCode = @"
            public static System.Collections.Generic.IEnumerable<System.Xml.Linq.XElement> GetElementByIndex(System.Xml.Linq.XContainer doc, System.Xml.Linq.XName element, int index)
            {
                return doc.Descendants(element)
                        .Where  (e => e.NodesBeforeSelf().Count() == index)
                        .Select (e => e);
            }
            public static System.Collections.Generic.IEnumerable<System.Xml.Linq.XElement> GetElementByValue(System.Xml.Linq.XContainer doc, System.Xml.Linq.XName element, string value)
            {
                return  doc.Descendants(element) 
                        .Where  (e => e.Value == value)
                        .Select (e => e);
            }
            public static System.Collections.Generic.IEnumerable<System.Xml.Linq.XElement> GetOddElements(System.Xml.Linq.XContainer doc, System.Xml.Linq.XName element)
            {
                return doc.Descendants(element)
                        .Where  ((e,i) => i % 2 != 0)
                        .Select (e => e);
            }
            public static System.Collections.Generic.IEnumerable<System.Xml.Linq.XElement> GetEvenElements(System.Xml.Linq.XContainer doc, System.Xml.Linq.XName element)
            {
                return doc.Descendants(element)
                        .Where  ((e,i) => i % 2 == 0)
                        .Select (e => e);
            }
"@
            try
            {
                Add-Type -ErrorAction SilentlyContinue `
                -ReferencedAssemblies System.Xml, System.Xml.Linq `
                -UsingNamespace System.Linq `
                -Name XUtilities `
                -Namespace CustomLinq `
                -MemberDefinition $LinqCode
                
                $LinqAssemblyLoaded = $true
            }
            catch
            {
                $LinqAssemblyLoaded = $false
            }
        }
        $tablepattern = [regex]'(?s)(<table.*?>.*?</table>)'
        $headerpattern = [regex]'(?s)(^.*?)(?=<table)'
        $footerpattern = [regex]'(?s)(?<=</table>)(.*?$)'
        $header = ''
        $footer = ''
    }
    PROCESS 
    { }
    END 
    { 
        if ($psCmdlet.ParameterSetName -eq 'ObjectSet')
        {
            # If we sent an array of objects convert it to html first
            $InputString = ($InputObject | ConvertTo-Html -Head $HTMLHead)
        }

        # Convert our data to x(ht)ml 
        if ($LinqAssemblyLoaded)
        {
            $xml = [System.Xml.Linq.XDocument]::Parse("$InputString")
        }
        else
        {
            # old school xml is kinda dumb so we strip out only the table to work with then 
            # add the header and footer back on later.
            $firsttable = [Regex]::Match([string]$InputString, $tablepattern).Value
            $header = [Regex]::Match([string]$InputString, $headerpattern).Value
            $footer = [Regex]::Match([string]$InputString, $footerpattern).Value
            [xml]$xml = [string]$firsttable
        }
        switch ($ColorizeMethod) {
            "ByEvenRows" {
                if ($LinqAssemblyLoaded)
                {
                    $evenrows = [CustomLinq.XUtilities]::GetEvenElements($xml, "{http://www.w3.org/1999/xhtml}tr")    
                    foreach ($row in $evenrows)
                    {
                        $row.SetAttributeValue($Attr, $AttrValue)
                    }
                }
                else
                {
                    $rows = $xml.GetElementsByTagName('tr')
                    for($i=0;$i -lt $rows.count; $i++)
                    {
                        if (($i % 2) -eq 0 ) {
                           $newattrib=$xml.CreateAttribute($Attr)
                           $newattrib.Value=$AttrValue
                           [void]$rows.Item($i).Attributes.Append($newattrib)
                        }
                    }
                }
            }
            "ByOddRows" {
                if ($LinqAssemblyLoaded)
                {
                    $oddrows = [CustomLinq.XUtilities]::GetOddElements($xml, "{http://www.w3.org/1999/xhtml}tr")    
                    foreach ($row in $oddrows)
                    {
                        $row.SetAttributeValue($Attr, $AttrValue)
                    }
                }
                else
                {
                    $rows = $xml.GetElementsByTagName('tr')
                    for($i=0;$i -lt $rows.count; $i++)
                    {
                        if (($i % 2) -ne 0 ) {
                           $newattrib=$xml.CreateAttribute($Attr)
                           $newattrib.Value=$AttrValue
                           [void]$rows.Item($i).Attributes.Append($newattrib)
                        }
                    }
                }
            }
            "ByValue" {
                if ($LinqAssemblyLoaded)
                {
                    # Find the index of the column you want to format 
                    $ColumnLoc = [CustomLinq.XUtilities]::GetElementByValue($xml, "{http://www.w3.org/1999/xhtml}th",$Column) 
                    $ColumnIndex = $ColumnLoc | Foreach-Object{($_.NodesBeforeSelf() | Measure-Object).Count} 
            
                    # Process each xml element based on the index for the column we are highlighting 
                    switch([CustomLinq.XUtilities]::GetElementByIndex($xml, "{http://www.w3.org/1999/xhtml}td", $ColumnIndex)) 
                    { 
                        {$(Invoke-Command $ScriptBlock -ArgumentList @($_.Value, $ColumnValue))} {
                            if ($WholeRow)
                            {
                                $_.Parent.SetAttributeValue($Attr, $AttrValue)
                            }
                            else
                            {
                                $_.SetAttributeValue($Attr, $AttrValue)
                            }
                        }
                    }
                }
                else
                {
                    $colvalindex = 0
                    $headerindex = 0
                    $xml.GetElementsByTagName('th') | Foreach {
                        if ($_.'#text' -eq $Column) 
                        {
                            $colvalindex=$headerindex
                        }
                        $headerindex++
                    }
                    $rows = $xml.GetElementsByTagName('tr')
                    $cols = $xml.GetElementsByTagName('td')
                    $colvalindexstep = ($cols.count /($rows.count - 1))
                    for($i=0;$i -lt $rows.count; $i++)
                    {
                        $index = ($i * $colvalindexstep) + $colvalindex
                        $colval = $cols.Item($index).'#text'
                        if ($(Invoke-Command $ScriptBlock -ArgumentList @($colval, $ColumnValue))) {
                            $newattrib=$xml.CreateAttribute($Attr)
                            $newattrib.Value=$AttrValue
                            try 
                            {
                                if ($WholeRow)
                                {
                                    [void]$rows.Item($i).Attributes.Append($newattrib)
                                }
                                else
                                {
                                    [void]$cols.Item($index).Attributes.Append($newattrib)
                                }
                            }
                            catch
                            {
                                Write-Warning -Message ('Format-HTMLTable: Something weird happened! - {0}' -f $_.Exception.Message)
                            }
                        }
                    }
                }
            }
        }
        if ($LinqAssemblyLoaded)
        {
            if ($Fragment)
            {
                [string]$htmlresult = $xml.Document.ToString()
                if ([string]$htmlresult -match $tablepattern)
                {
                    [string]$matches[0]
                }
            }
            else
            {
                [string]$xml.Document.ToString()
            }
        }
        else
        {
            if ($Fragment)
            {
                [string]($xml.OuterXml | Out-String)
            }
            else
            {
                [string]$htmlresult = $header + ($xml.OuterXml | Out-String) + $footer
                return $htmlresult
            }
        }
    }
}

Function Add-Zip
{
    param([string]$zipfilename)

    if(-not (test-path($zipfilename)))
    {
        set-content $zipfilename ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
        (dir $zipfilename).IsReadOnly = $false  
    }

    $shellApplication = new-object -com shell.application
    $zipPackage = $shellApplication.NameSpace($zipfilename)

    foreach($file in $input) 
    { 
            $zipPackage.CopyHere($file.FullName)
            Start-sleep -milliseconds 500
    }
}

Function New-ZipFile
{
    #.Synopsis
    #  Expand a zip file, ensuring it's contents go to a single folder ...
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory=$true)]
        $ZipFilePath,

        [Parameter(Position=1, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Alias("PSPath","Item")]
        [string[]]
        $InputObject = $Pwd,

        [switch]
        $Append,

        # The compression level (defaults to Optimal):
        #   Optimal - The compression operation should be optimally compressed, even if the operation takes a longer time to complete.
        #   Fastest - The compression operation should complete as quickly as possible, even if the resulting file is not optimally compressed.
        #   NoCompression - No compression should be performed on the file.
        [System.IO.Compression.CompressionLevel]$Compression = "Optimal"
    )
    BEGIN
    {
        # Make sure the folder already exists
        [string]$File = Split-Path $ZipFilePath -Leaf
        [string]$Folder = $(if($Folder = Split-Path $ZipFilePath) { Resolve-Path $Folder } else { $Pwd })
        $ZipFilePath = Join-Path $Folder $File
        # If they don't want to append, make sure the zip file doesn't already exist.
        if(!$Append) 
        {
            if(Test-Path $ZipFilePath) 
            { 
                Remove-Item $ZipFilePath 
            }
        }
        $Archive = [System.IO.Compression.ZipFile]::Open( $ZipFilePath, "Update" )
    }
    PROCESS
    {
        foreach($path in $InputObject) 
        {
            foreach($item in Resolve-Path $path) 
            {
                # Push-Location so we can use Resolve-Path -Relative 
                Push-Location (Split-Path $item)
                # This will get the file, or all the files in the folder (recursively)
                foreach($file in Get-ChildItem $item -Recurse -File -Force | % FullName) 
                {
                    # Calculate the relative file path
                    $relative = (Resolve-Path $file -Relative).TrimStart(".\")
                    # Add the file to the zip
                    $null = [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $file, $relative, $Compression)
                }
                Pop-Location
            }
        }
    }
    END
    {
        $Archive.Dispose()
        Get-Item $ZipFilePath
    }
}
#endregion Functions - Serial or Utility

#region Functions - AD
Function Search-AD
{
# Original Author (largely unmodified btw): 
#  http://becomelotr.wordpress.com/2012/11/02/quick-active-directory-search-with-pure-powershell/
    param (
        [string[]]$Filter,
        [string[]]$Properties = @('Name','ADSPath'),
        [string]$SearchRoot,
        [switch]$DontJoinAttributeValues
    )
    if ($SearchRoot) 
    { 
        $Root = [ADSI]$SearchRoot
    }
    else 
    {
        $Root = [ADSI]''
    }
    if ($Filter)
    {
        $LDAP = "(&({0}))" -f ($Filter -join ')(')
    }
    else
    {
        $LDAP = "(name=*)"
    }
    try
    {
        (New-Object ADSISearcher -ArgumentList @(
            $Root,
            $LDAP,
            $Properties
        ) -Property @{
            PageSize = 1000
        }).FindAll() | ForEach-Object {
            $ObjectProps = @{}
            $_.Properties.GetEnumerator() |
                Foreach-Object {
                    $Val = @($_.Value)
                    if ($_.Name -ne $null)
                    {
                        if ($DontJoinAttributeValues -and ($Val.Count -gt 1))
                        {
                            $ObjectProps.Add(
                                $_.Name,
                                ($_.Value)
                            )
                        }
                        else
                        {
                            $ObjectProps.Add(
                                $_.Name,
                                (-join $_.Value)
                            )
                        }
                    }
                }
            if ($ObjectProps.psbase.keys.count -ge 1)
            {
                New-Object PSObject -Property $ObjectProps |
                    select $Properties
            }
        }
    }
    catch
    {
        Write-Warning -Message ('Search-AD: Filter - {0}: Root - {1}: Error - {2}' -f $LDAP,$Root.Path,$_.Exception.Message)
    }
}

Function Append-ADUserAccountControl 
{
    <#
        author: Zachary Loeber
        http://support.microsoft.com/kb/305144
        http://msdn.microsoft.com/en-us/library/cc245514.aspx
    #>
    [cmdletbinding()]
    param
    (
        [Parameter(HelpMessage='User or users to process.',
                   Mandatory=$true,
                   ValueFromPipeline=$true)]
        [psobject[]]$User
    )

    BEGIN
    {
        Add-Type -TypeDefinition @" 
        [System.Flags]
        public enum userAccountControlFlags {
            SCRIPT                                  = 0x0000001,
            ACCOUNTDISABLE                          = 0x0000002,
            NOT_USED                                = 0x0000004,
            HOMEDIR_REQUIRED                        = 0x0000008,
            LOCKOUT                                 = 0x0000010,
            PASSWD_NOTREQD                          = 0x0000020,
            PASSWD_CANT_CHANGE                      = 0x0000040,
            ENCRYPTED_TEXT_PASSWORD_ALLOWED         = 0x0000080,
            TEMP_DUPLICATE_ACCOUNT                  = 0x0000100,
            NORMAL_ACCOUNT                          = 0x0000200,
            INTERDOMAIN_TRUST_ACCOUNT               = 0x0000800,
            WORKSTATION_TRUST_ACCOUNT               = 0x0001000,
            SERVER_TRUST_ACCOUNT                    = 0x0002000,
            DONT_EXPIRE_PASSWD                      = 0x0010000,
            MNS_LOGON_ACCOUNT                       = 0x0020000,
            SMARTCARD_REQUIRED                      = 0x0040000,
            TRUSTED_FOR_DELEGATION                  = 0x0080000,
            NOT_DELEGATED                           = 0x0100000,
            USE_DES_KEY_ONLY                        = 0x0200000,
            DONT_REQUIRE_PREAUTH                    = 0x0400000,
            PASSWORD_EXPIRED                        = 0x0800000,
            TRUSTED_TO_AUTH_FOR_DELEGATION          = 0x1000000
        }
"@
        $Users = @()
        $UACAttribs = @(
            'SCRIPT',
            'ACCOUNTDISABLE',
            'NOT_USED',
            'HOMEDIR_REQUIRED',
            'LOCKOUT',
            'PASSWD_NOTREQD',
            'PASSWD_CANT_CHANGE',
            'ENCRYPTED_TEXT_PASSWORD_ALLOWED',
            'TEMP_DUPLICATE_ACCOUNT',
            'NORMAL_ACCOUNT',
            'INTERDOMAIN_TRUST_ACCOUNT',
            'WORKSTATION_TRUST_ACCOUNT',
            'SERVER_TRUST_ACCOUNT',
            'DONT_EXPIRE_PASSWD',
            'MNS_LOGON_ACCOUNT',
            'SMARTCARD_REQUIRED',
            'TRUSTED_FOR_DELEGATION',
            'NOT_DELEGATED',
            'USE_DES_KEY_ONLY',
            'DONT_REQUIRE_PREAUTH',
            'PASSWORD_EXPIRED',
            'TRUSTED_TO_AUTH_FOR_DELEGATION',
            'PARTIAL_SECRETS_ACCOUNT'
        )
    }
    PROCESS
    {
        $Users += $User
    }
    END
    {
        Foreach ($usr in $Users)
        {
            if ($usr.PSObject.Properties.Match('useraccountcontrol').Count) 
            {
                try 
                {
                    $UAC = [Enum]::Parse('userAccountControlFlags', $usr.useraccountcontrol)
                    $UACAttribs | Foreach {
                        Add-Member -InputObject $usr -MemberType NoteProperty `
                        -Name $_ -Value ($UAC -match $_) -Force
                    }
                }
                catch
                {
                    Write-Warning -Message ('Append-ADUserAccountControl: {0}' -f $_.Exception.Message)
                }
            }
            $usr
        }
    }
}

Function Normalize-ADUsers
{
    [cmdletbinding()]
    param
    (
        [Parameter(HelpMessage='User or users to process.',
                   Mandatory=$true,
                   ValueFromPipeline=$true)]
        [psobject[]]$User,
        
        [Parameter(HelpMessage='AD attributes to process.',
                   Mandatory=$true)]
        [string[]]$Attribs
    )

    BEGIN
    {
        $Users = @()
        $LyncPools = Get-LyncPoolAssociationHash | 
                      ConvertTo-HashArray -PivotProperty 'ServiceName' -LookupValue 'PoolName'
    }
    PROCESS
    {
        if ($User -ne $null)
        {
            $Users += $User
        }
    }
    END
    {
        Foreach ($usr in $Users)
        {
            $UserProps = @{}
            Foreach ($Attrib in $Attribs)
            {
                if ($usr.PSObject.Properties.Match($Attrib).Count) 
                {
                    switch ($Attrib) 
                    {
                        'pwdlastset' {                            
                            $AttribVal = [datetime]::FromFileTime([int64]($usr.$Attrib))
                            $PasswordAge=((get-date) - $AttribVal).days
                            $UserProps.Add(
                                'PasswordAge',
                                $PasswordAge
                            )
                            break
                        }
                        'lastlogontimestamp' {
                            $AttribVal = [datetime]::FromFileTime([int64]($usr.$Attrib))
                            if ($AttribVal -match '12/31/1600')
                            {
                                $LogonAge = 'Never'
                                $AttribVal = 'Never'
                            }
                            else
                            {
                                $LogonAge=((get-date) - $AttribVal).days
                            }
                            $UserProps.Add(
                                'DaysSinceLastLogon',
                                $LogonAge
                            )
                            break
                        }
                        { @('badPasswordTime', 'lastlogon') -contains $_ } {
                            $AttribVal = [datetime]::FromFileTime([int64]($usr.$Attrib))
                            break
                        }
                        'accountExpires'{
                            if (($usr.$Attrib -eq 0) -or ($usr.$Attrib -eq '9223372036854775807') -or ($usr.$Attrib -eq '9223372032559808511'))
                            {
                                $AttribVal = 'Never'
                            }
                            else
                            {
                                $AttribVal = [datetime]::FromFileTime([int64]($usr.$Attrib))
                            }
                            break
                        }
                        'msRTCSIP-PrimaryHomeServer' {
                            if ($usr.$Attrib -ne $null)
                            {
                                $AttribVal = $LyncPools[$usr.$Attrib]
                            }
                            else
                            {
                                $AttribVal = $null
                            }
                            $UserProps.Add(
                                'LyncPool',
                                $AttribVal
                            )
                        }
                        default {
                            $AttribVal = $usr.$Attrib
                            break
                        }
                     }

                    $UserProps.Add(
                            $Attrib,
                            $AttribVal
                    )
                } 
                else 
                { 
                    $UserProps.Add(
                            $Attrib,
                            $null
                    )
                }
            }
            New-Object psobject -Property $UserProps
        }
    }
}

Function Get-ADPrivilegedGroups
{
    [CmdletBinding()]
    param
    (
        [Parameter(HelpMessage="Domain to gather privileged group information about. If not specified, all domains in the current forest will be enumerated.",
                   Mandatory=$false,
                   ValueFromPipeline=$true)]
        $Domain
    )
    BEGIN
    {
        $Domains = @()
    }
    PROCESS
    {
        if ($Domain -ne $null)
        {
            $Domains += $Domain
        }
    }
    END
    {
        if ($Domains.Count -eq 0)
        {
            $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            $Domains = @($Forest.Domains | %{[string]$_.Name})
        }
        Foreach ($Dom in $Domains)
        {
            # Domain SID
            $DomainDN = 'dc=' + $Dom.Replace('.', ',dc=')
            $DomGCobject = [adsi]"GC://$domainDN"
            $DomSid = New-Object System.Security.Principal.SecurityIdentifier($DomGCobject.objectSid[0], 0)
            $DomSid = $DomSid.toString()
            
            $StaticPrivGroupDesc = @{
                'S-1-5-32-544' = "Administrators"
                'S-1-5-32-548' = "Account Operators"
                'S-1-5-32-549' = "Server Operators"
                'S-1-5-32-550' = "Print Operators"
                'S-1-5-32-551' = "Backup Operators"
                "$DomSid-517" = "Cert Publishers"
                "$DomSid-518"  = "Schema Admins"
                "$DomSid-519"  = "Enterprise Admins"
               # "$DomSid-520"  = "Group Policy Creator Owners"
                "$DomSid-512"  = "Domain Admins"
            }
            $ADProp_Grp = @('Name',
                            'cn',
                            'distinguishedname')
            
            Foreach ($GrpSid in $StaticPrivGroupDesc.Keys)
            {
                $Grp = @(Search-AD -Filter "(objectSID=$GrpSid)" `
                                   -SearchRoot "LDAP://$DomainDN" `
                                   -Properties $ADProp_Grp)
                if ($Grp.Count -gt 0)
                {
                    $GrpProps = @{
                        'Domain' = $dom
                        'Group' = $StaticPrivGroupDesc[$GrpSid]
                        'GroupDN' = $Grp[0].distinguishedname
                        'GroupCN' = $Grp[0].cn
                        'GroupName' = $Grp[0].Name
                #        'Admincount' = $Grp[0].admincount
                        'Sid' = $GrpSid
                    }
                    New-Object PSObject -Property $GrpProps
                }
            }
        }
    }
}

Function Get-ADDomainPrivAccounts
{
    [CmdletBinding()]
    param
    (
        [Parameter(HelpMessage="Domain to gather privileged accounts. If not specified, all domains in the current forest will be enumerated.",
                   ValueFromPipeline=$true)]
        [string[]]$Domain,
        [Parameter(HelpMessage='User attributes to include in results.')]
        $UserAttribs = @( 'cn',
                          'displayName',
                          'givenName',
                          'sn',
                          'name',
                          'sAMAccountName',
                          'whenChanged',
                          'whenCreated',
                          'pwdLastSet',
                          'badPasswordTime',
                          'badPwdCount',
                          'lastLogon',
                          'logonCount',
                          'useraccountcontrol',
                          'lastlogontimestamp'
                        )
    )
    BEGIN
    {
        $RootDSC = [adsi]"LDAP://RootDSE"
        $DomNamingContext = $RootDSC.RootDomainNamingContext
        $ConfigNamingContext = $RootDSC.configurationNamingContext
        $Domains = @()
    }
    PROCESS
    {
        if ($Domain -ne $null)
        {
            $Domains += $Domain
        }
    }
    END
    {
        if ($Domains.Count -eq 0)
        {
            $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            $Domains = @($Forest.Domains | %{[string]$_.Name})
        }
        $DomPrivGroups = @()
        ForEach ($Dom in $Domains) 
        { 
            $CurDomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $Dom)
            $CurDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($CurDomainContext)
            $CurDomainDetails = [ADSI]"LDAP://$($CurDomain)"
            $DomainDN = 'dc=' + $Dom.Replace('.', ',dc=')
            $NetBIOSName = Get-NETBiosName $DomainDN $ConfigNamingContext
            
            $DomPrivGroups = @(Get-ADPrivilegedGroups -Domain $Dom)
            Foreach ($PrivGroup in $DomPrivGroups)
            {
                $PrivGroupDN = $PrivGroup.GroupDN
                Write-Verbose $PrivGroupDN
                # Only works on 2003 SP2 and above
                $Filter = "(samAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(memberOf:1.2.840.113556.1.4.1941:=$PrivGroupDN)"
                $PrivUsers = @(Search-AD -Filter $Filter `
                                         -SearchRoot "LDAP://$DomainDN" `
                                         -Properties $UserAttribs)
                Write-Verbose -Message ('Privileged Users: Group {0}' -f $PrivGroup.GroupDN)
                $PrivUsers = $PrivUsers | 
                             Normalize-ADUsers -Attribs $UserAttribs |
                             Append-ADUserAccountControl
                Foreach ($PrivUser in $PrivUsers)
                {
                    if ($PrivUser -ne $null)
                    {
                        $PrivMemberProp = @{
                            Domain = $Dom
                            DomainNetBIOS = $NetBIOSName
                            PrivGroup = $PrivGroup.Group
                        }
                        $PrivUser.psobject.properties | 
                        Where {$_.Name -ne $null} | ForEach {
                            $PrivMemberProp[$_.Name] = $_.Value 
                        }
                        New-Object psobject -Property $PrivMemberProp
                    }
                }
            }
        }
    }
}

Function Get-TreeFromLDAPPath
{
    # $Output = [System.Web.HttpUtility]::HtmlDecode(($a | ConvertTo-Html))
    [CmdletBinding()]
    Param
    (
        [Parameter(HelpMessage="LDAP path.")]
        [string]
        $LDAPPath,
        
        [Parameter(HelpMessage="Determines the depth a tree node is indented")]
        [int]
        $IndentDepth=1,
        
        [Parameter(HelpMessage="Optional character to use for each newly indented node.")]
        [char]
        $IndentChar = 3,
        
        [Parameter(HelpMessage="Don't remove the ldap node type (ie. DC=)")]
        [Switch]
        $KeepNodeType
     )
    $regex = [regex]'(?<LDAPType>^.+)\=(?<LDAPName>.+$)'
    $ldaparr = Get-ADPathName $LDAPPath -split
    $ADPartCount = $ldaparr.count
    $spacer = ''
    $output = ''
    for ($index = ($ADPartCount); $index -gt 0; $index--) 
    {
        $node = $ldaparr[($index-1)]
        if (-not $KeepNodeType)
        {
            if ($node -match $regex)
            {
                $node = $matches['LDAPName']
            }
        }
        if ($index -eq ($ADPartCount))
        {
            $line = ''
        }
        else
        {
            $line = $IndentChar
            $spacer = $spacer + (' ' * $IndentDepth)
            # This fixes an offset issue
            if ($index -lt ($ADPartCount - 1))
            {
                $spacer = $spacer + ' '
            }
        }
        $line = $spacer + $line + $node + "`n"
        $output = $Output+$line
    }
    [string]$output
}

Function Get-ObjectFromLDAPPath
{
    [CmdletBinding()]
    Param
    (
        [Parameter(HelpMessage="LDAP path.")]
        [string]
        $LDAPPath,
        
        [Parameter(HelpMessage="Translate the ldap type.")]
        [switch]
        $TranslateNamingAttribute
    )
    $output = @()
    $ldaparr = Get-ADPathName $LDAPPath -split
    $regex = [regex]'(?<LDAPType>^.+)\=(?<LDAPName>.+$)'
    $position = 0
    $ldaparr | %{
        if ($_ -match $regex)
        {
            if ($TranslateNamingAttribute)
            {
                switch ($matches['LDAPType']) 
                {
                      'CN' {$_ldaptype = "Common Name"}
                      'OU' {$_ldaptype = "Organizational Unit"}
                      'DC' {$_ldaptype = "Domain Component"}
                   default {$_ldaptype = $matches['LDAPType']}
                }
            }
            else
            {
                $_ldaptype = $matches['LDAPType']
            }
            $objprop = @{
                LDAPType = $_ldaptype
                LDAPName = $matches['LDAPName']
                Position = $position
            }
            $output += New-Object psobject -Property $objprop
            $position++
        }
    }
    Write-Output -InputObject $output
}

Function Get-LyncPoolAssociationHash 
{
    BEGIN
    {
        $Lync_Elements = @()
        $AD_PoolProperties = @('cn',
                               'distinguishedName',
                               'dnshostname',
                               'msrtcsip-pooldisplayname'
                              )
    }
    PROCESS
    {}
    END
    {
        $RootDSC = [adsi]"LDAP://RootDSE"
        $DomNamingContext = $RootDSC.RootDomainNamingContext
        $ConfigNamingContext = $RootDSC.configurationNamingContext
        $OCSADContainer = ''

        # Find Lync AD config partition 
        $LyncPathSearch = @(Search-AD -Filter '(objectclass=msRTCSIP-Service)' -SearchRoot "LDAP://$([string]$DomNamingContext)")
        if ($LyncPathSearch.count -ge 1)
        {
            $OCSADContainer = ($LyncPathSearch[0]).adspath
        }
        else
        {
            $LyncPathSearch = @(Search-AD -Filter '(objectclass=msRTCSIP-Service)' -SearchRoot "LDAP://$ConfigNamingContext")
            if ($LyncPathSearch.count -ge 1)
            {
                $OCSADContainer = ($LyncPathSearch[0]).adspath
            }
        }
        if ($OCSADContainer -ne '')
        {
            $LyncPoolLookupTable = @{}
            # All Lync pools
            $Lync_Pools = @(Search-AD -Filter '(&(objectClass=msRTCSIP-Pool))' `
                                      -Properties $AD_PoolProperties `
                                      -SearchRoot $OCSADContainer)
            $LyncPoolCount = $Lync_Pools.Count
            $Lync_Pools | %{
                $LyncElementProps = @{
                    CN = $_.cn
                    distinguishedName = $_.distinguishedName
                    ServiceName = "CN=Lc Services,CN=Microsoft,$($_.distinguishedName)"
                    PoolName = $_.'msrtcsip-pooldisplayname'
                    PoolFQDN = $_.dnshostname
                }
                $Lync_Elements += New-Object PSObject -Property $LyncElementProps
            }
            $Lync_Elements
        }
    }
}

Function Get-NETBiosName ( $dn, $ConfigurationNC ) 
{ 
    try 
    { 
        $Searcher = New-Object System.DirectoryServices.DirectorySearcher  
        $Searcher.SearchScope = "subtree"  
        $Searcher.PropertiesToLoad.Add("nETBIOSName")| Out-Null 
        $Searcher.SearchRoot = "LDAP://cn=Partitions,$ConfigurationNC" 
        $Searcher.Filter = "(nCName=$dn)" 
        $NetBIOSName = ($Searcher.FindOne()).Properties.Item("nETBIOSName") 
        Return $NetBIOSName 
    } 
    catch 
    { 
        Return $null 
    } 
}

Function Get-ADPathName
{
    # Get-ADPathname
    # Written by Bill Stewart (bstewart@iname.com)
    # PowerShell wrapper script for the Pathname COM object.

    #requires -version 2

    <#
    .SYNOPSIS
    Outputs Active Directory path names in various formats.

    .DESCRIPTION
    Outputs Active Directory (AD) path names in various formats using the Pathname COM object. The Pathname COM object implements the ADSI IADSPathname interface (see RELATED LINKS). This is a more robust means of handling AD path names than string parsing because it supports escaping of special characters.

    .PARAMETER Path
    Specifies the AD path. For example: "CN=Ken Dyer,DC=fabrikam,DC=com". If using the Full type (see -Full parameter), include the server and/or provider; for example: "LDAP://CN=Ken Dyer,DC=fabrikam,DC=com" or "LDAP://server/CN=Key Dyer,DC=fabrikam,DC=com".

    .PARAMETER Type
    Specifies the type of the AD path. This parameter must be one of the following values: "DN" or "Full". If you specify "Full", include the provider and/or server. The default value for this parameter is "DN".

    .PARAMETER Format
    Specifies the format in which to output the AD path. This parameter must be one of the following values: "Windows", "WindowsNoServer", "WindowsDN", "WindowsParent", "X500", "X500NoServer", "X500DN", "X500Parent", "Server", "Provider", or "Leaf". The default value for this parameter is "X500DN" (i.e., the distinguished name of the user, without provider or server names). This parameter's values correspond to the ADS_FORMAT_ENUM enumeration's values (see RELATED LINKS for more information and examples).

    .PARAMETER Retrieve
    Outputs the AD path using the format specified by the -Format parameter. This parameter is optional.

    .PARAMETER AddLeafElement
    Adds the specified leaf element(s) to the AD path and outputs the new AD path(s) using the format specified by the -Format parameter.

    .PARAMETER RemoveLeafElement
    Removes the final leaf element from the AD path and outputs the new AD path(s) using the format specified by the -Format parameter.

    .PARAMETER GetElement
    Outputs the specified element from the AD path. The left-most element is numbered 0 (zero), the second is numbered 1 (one), and so forth.

    .PARAMETER GetNumElements
    Outputs the number of elements in the AD path.

    .PARAMETER Split
    Outputs a list of the elements in the AD path.

    .PARAMETER GetEscapedElement
    Outputs one or more AD name element(s) with escape ("\") characters inserted in the correct places.

    .PARAMETER EscapedMode
    Specifies how escape characters are displayed in the AD path. This parameter must be one of the following values: "Default", "On", "Off", or "OffEx". The default value for this parameter is "Default".

    .PARAMETER ValuesOnly
    Specifies how elements in a path are output. If this parameter is absent, path elements are output using both attributes and values (e.g., "CN=Ken Dyer"). If this parameter is present, path elements are output with values only (e.g., "Ken Dyer").

    .INPUTS
    Inputs are AD path strings.

    .OUTPUTS
    Outputs are AD path strings.

    .EXAMPLE
    PS C:\> Get-ADPathname "LDAP://CN=Ken Dyer,CN=Users,DC=fabrikam,DC=com" -Type Full -Retrieve -Format X500DN
    Outputs "CN=Ken Dyer,CN=Users,DC=fabrikam,DC=com". The -Type parameter indicates that the AD path contains a provider (LDAP), and -Retrieve retrieves the path without the provider. The -Retrieve and -Format parameters are optional.

    .EXAMPLE
    PS C:\> Get-ADPathname "CN=Ken Dyer,CN=Users,DC=fabrikam,DC=com" -RemoveLeafElement
    This command removes the last element from the AD path ("CN=Ken Dyer") and outputs "CN=Users,DC=fabrikam,DC=com".

    .EXAMPLE
    PS C:\> Get-ADPathname "CN=Jeff Smith,CN=H/R,DC=fabrikam,DC=com" -EscapedMode On
    This command escapes the needed characters in the AD path and outputs "CN=Jeff Smith,CN=H\/R,DC=fabrikam,DC=com".

    .EXAMPLE
    PS C:\> Get-ADPathname "CN=H/R,DC=fabrikam,DC=com" -AddLeafElement "CN=Jeff Smith"
    This command adds the leaf element to the AD path and outputs "CN=Jeff Smith,CN=H/R,DC=fabrikam,DC=com".

    .EXAMPLE
    PS C:\> Get-ADPathname "CN=Jeff Smith,CN=H/R,DC=fabrikam,DC=com" -RemoveLeafElement
    This command removes the last element from the AD path ("CN=Jeff Smith") and outputs "CN=H/R,DC=fabrikam,DC=com".

    .EXAMPLE
    PS C:\> Get-ADPathname "CN=Ken Dyer,CN=Users,DC=fabrikam,DC=com" -Split
    This command splits the AD path and outputs a list of the elements: "CN=Ken Dyer", "CN=Users", "DC=fabrikam", and "DC=com".

    .EXAMPLE
    PS C:\> Get-Content ADPaths.txt | Get-ADPathname -EscapedMode On
    This command outputs all of the AD paths listed in the file ADPaths.txt with the needed escape characters.

    .EXAMPLE
    PS C:\> Get-ADPathname "CN=Users,DC=fabrikam,DC=com" -GetElement 0 -ValuesOnly
    This command gets the left-most element from the path and outputs "Users". Without the -ValuesOnly parameter, this command will output "CN=Users".

    .EXAMPLE
    PS C:\> Get-ADPathname -GetEscapedElement "OU=H/R"
    This command inserts the needed escape characters and outputs "OU=H\/R".

    .LINK
    ADSI IADSPathname Interface - http://msdn.microsoft.com/en-us/library/windows/desktop/aa706070.aspx
    ADS_FORMAT_ENUM Enumeration - http://msdn.microsoft.com/en-us/library/windows/desktop/aa772261.aspx
    #>

    [CmdletBinding(DefaultParameterSetName="Retrieve")]
    param(
      [parameter(ParameterSetName="Retrieve",Position=0,ValueFromPipeline=$TRUE)]
      [parameter(ParameterSetName="AddLeafElement",Position=0,Mandatory=$TRUE)]
      [parameter(ParameterSetName="RemoveLeafElement",Position=0,Mandatory=$TRUE)]
      [parameter(ParameterSetName="GetElement",Position=0,Mandatory=$TRUE)]
      [parameter(ParameterSetName="GetNumElements",Position=0,Mandatory=$TRUE)]
      [parameter(ParameterSetName="Split",Position=0,Mandatory=$TRUE)]
        [String[]]
        $Path,
      [parameter(ParameterSetName="Retrieve")]
      [parameter(ParameterSetName="AddLeafElement")]
      [parameter(ParameterSetName="RemoveLeafElement")]
      [parameter(ParameterSetName="GetElement")]
      [parameter(ParameterSetName="GetNumElements")]
      [parameter(ParameterSetName="Split")]
        [String] [ValidateSet("DN","Full")]
        $Type,
      [parameter(ParameterSetName="Retrieve")]
        [Switch]
        $Retrieve,
      [parameter(ParameterSetName="AddLeafElement",Mandatory=$TRUE)]
        [String[]]
        $AddLeafElement,
      [parameter(ParameterSetName="GetElement",Mandatory=$TRUE)]
        [UInt32]
        $GetElement,
      [parameter(ParameterSetName="RemoveLeafElement",Mandatory=$TRUE)]
        [Switch]
        $RemoveLeafElement,
      [parameter(ParameterSetName="GetNumElements",Mandatory=$TRUE)]
        [Switch]
        $GetNumElements,
      [parameter(ParameterSetName="Split",Mandatory=$TRUE)]
        [Switch]
        $Split,
      [parameter(ParameterSetName="Retrieve")]
      [parameter(ParameterSetName="AddLeafElement")]
      [parameter(ParameterSetName="RemoveLeafElement")]
        [String] [ValidateSet("Windows","WindowsNoServer","WindowsDN","WindowsParent","X500","X500NoServer","X500DN","X500Parent","Server","Provider","Leaf")]
        $Format,
      [parameter(ParameterSetName="Retrieve")]
      [parameter(ParameterSetName="AddLeafElement")]
      [parameter(ParameterSetName="RemoveLeafElement")]
      [parameter(ParameterSetName="GetElement")]
      [parameter(ParameterSetName="Split")]
        [String] [ValidateSet("Default","On","Off","OffEx")]
        $EscapedMode,
      [parameter(ParameterSetName="Retrieve")]
      [parameter(ParameterSetName="AddLeafElement")]
      [parameter(ParameterSetName="RemoveLeafElement")]
      [parameter(ParameterSetName="GetElement")]
      [parameter(ParameterSetName="Split")]
        [Switch]
        $ValuesOnly,
      [parameter(ParameterSetName="GetEscapedElement",Mandatory=$TRUE)]
        [String[]]
        $GetEscapedElement
    )

    begin {
      $ParamSetName = $PSCMDLET.ParameterSetName

      # Determine if we're using pipeline input.
      $PipelineInput = $FALSE
      if ( $ParamSetName -eq "Retrieve" ) {
        $PipelineInput = -not $PSBoundParameters.ContainsKey("Path")
      }

      # These hash tables improve code readability.
      $InputTypes = @{
        "Full" = 1
        "DN"   = 4
      }
      $OutputFormats = @{
        "Windows"         = 1 
        "WindowsNoServer" = 2 
        "WindowsDN"       = 3 
        "WindowsParent"   = 4 
        "X500"            = 5 
        "X500NoServer"    = 6 
        "X500DN"          = 7 
        "X500Parent"      = 8 
        "Server"          = 9 
        "Provider"        = 10
        "Leaf"            = 11
      }
      $EscapedModes = @{
        "Default" = 1
        "On"      = 2
        "Off"     = 3
        "OffEx"   = 4
      }
      $DisplayTypes = @{
        "Full"       = 1
        "ValuesOnly" = 2
      }

      # Invokes a method on a COM object that lacks a type library. If the COM
      # object uses more than one parameter, specify an array as the $parameters
      # parameter. The $outputType parameter coerces the function's output to the
      # specified type (default is [String]).
      function Invoke-Method {
        param(
          [__ComObject] $object,
          [String] $method,
          $parameters,
          [System.Type] $outputType = "String"
        )
        $output = $object.GetType().InvokeMember($method, "InvokeMethod", $NULL, $object, $parameters)
        if ( $output ) { $output -as $outputType }
      }

      # Sets a property on a COM object that lacks a type library.
      function Set-Property {
        param(
          [__ComObject] $object,
          [String] $property,
          $parameters
        )
        [Void] $object.GetType().InvokeMember($property, "SetProperty", $NULL, $object, $parameters)
      }

      # Creates the Pathname COM object. It lacks a type library so we use the
      # above Invoke-Method and Set-Property functions to interact with it.
      $Pathname = new-object -comobject "Pathname"

      # Set defaults for -Type and -Format. Use separate variables in case of
      # pipeline input.
      if ( $Type ) { $InputType = $Type } else { $InputType = "DN" }
      if ( $Format ) { $OutputFormat = $Format } else { $OutputFormat = "X500DN" }
      # Enable escaped mode if requested.
      if ( $EscapedMode ) {
        Set-Property $Pathname "EscapedMode" $EscapedModes[$EscapedMode]
      }
      # Output values only if requested.
      if ( $ValuesOnly ) {
        Invoke-Method $Pathname "SetDisplayType" $DisplayTypes["ValuesOnly"]
      }

      # -Retrieve parameter
      function Get-ADPathname-Retrieve {
        param(
          [String] $path,
          [Int] $inputType,
          [Int] $outputFormat
        )
        try {
          Invoke-Method $Pathname "Set" ($path,$inputType)
          Invoke-Method $Pathname "Retrieve" $outputFormat
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }

      # -AddLeafElement parameter
      function Get-ADPathname-AddLeafElement {
        param(
          [String] $path,
          [Int] $inputType,
          [String] $element,
          [Int] $outputFormat
        )
        try {
          Invoke-Method $Pathname "Set" ($path,$inputType)
          Invoke-Method $Pathname "AddLeafElement" $element
          Invoke-Method $Pathname "Retrieve" $outputFormat
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }

      # -RemoveLeafElement parameter
      function Get-ADPathname-RemoveLeafElement {
        param(
          [String] $path,
          [Int] $inputType,
          [Int] $outputFormat
        )
        try {
          Invoke-Method $Pathname "Set" ($path,$inputType)
          Invoke-Method $Pathname "RemoveLeafElement"
          Invoke-Method $Pathname "Retrieve" $outputFormat
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }

      # -GetElement parameter
      function Get-ADPathname-GetElement {
        param(
          [String] $path,
          [Int] $inputType,
          [Int] $elementIndex
        )
        try {
          Invoke-Method $Pathname "Set" ($path,$inputType)
          Invoke-Method $Pathname "GetElement" $elementIndex
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }

      # -GetNumElements parameter
      function Get-ADPathname-GetNumElements {
        param(
          [String] $path,
          [Int] $inputType
        )
        try {
          Invoke-Method $Pathname "Set" ($path,$inputType)
          Invoke-Method $Pathname "GetNumElements" -outputtype "UInt32"
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }

      # -Split parameter
      function Get-ADPathname-Split {
        param(
          [String] $path,
          [Int] $inputType
        )
        try {
          Invoke-Method $Pathname "Set" ($path,$inputType)
          $numElements = Invoke-Method $Pathname "GetNumElements" -outputtype "UInt32"
          for ( $i = 0; $i -lt $numElements; $i++ ) {
            Invoke-Method $Pathname "GetElement" $i
          }
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }

      # -GetEscapedElement parameter
      function Get-ADPathname-GetEscapedElement {
        param(
          [String] $element
        )
        try {
          Invoke-Method $Pathname "GetEscapedElement" (0,$element)
        }
        catch [System.Management.Automation.MethodInvocationException] {
          write-error -exception $_.Exception.InnerException
        }
      }
    }

    process {
      # The process block uses 'if'/'elseif' instead of 'switch' because 'switch'
      # replaces '$_', and we need '$_' in case of pipeline input.

      # "Retrieve" is the only parameter set that that accepts pipeline input.
      if ( $ParamSetName -eq "Retrieve" ) {
        if ( $PipelineInput ) {
          if ( $_ ) {
            Get-ADPathname-Retrieve $_ $InputTypes[$InputType] $OutputFormats[$OutputFormat]
          }
          else {
            write-error "You must provide pipeline input or specify the -Path parameter." -category SyntaxError
          }
        }
        else {
          $Path | foreach-object {
            Get-ADPathname-Retrieve $_ $InputTypes[$InputType] $OutputFormats[$OutputFormat]
          }
        }
      }
      elseif ( $ParamSetName -eq "AddLeafElement" ) {
        $AddLeafElement | foreach-object {
          Get-ADPathname-AddLeafElement $Path[0] $InputTypes[$InputType] $_ $OutputFormats[$OutputFormat]
        }
      }
      elseif ( $ParamSetName -eq "RemoveLeafElement" ) {
        $Path | foreach-object {
          Get-ADPathname-RemoveLeafElement $_ $InputTypes[$InputType] $OutputFormats[$OutputFormat]
        }
      }
      elseif ( $ParamSetName -eq "GetElement" ) {
        $Path | foreach-object {
          Get-ADPathname-GetElement $_ $InputTypes[$InputType] $GetElement
        }
      }
      elseif ( $ParamSetName -eq "GetNumElements" ) {
        $Path | foreach-object {
          Get-ADPathname-GetNumElements $_ $InputTypes[$InputType]
        }
      }
      elseif ( $ParamSetName -eq "Split" ) {
        Get-ADPathname-Split $Path[0] $InputTypes[$InputType]
      }
      elseif ( $ParamSetName -eq "GetEscapedElement" ) {
        $GetEscapedElement | foreach-object {
          Get-ADPathname-GetEscapedElement $_
        }
      }
    }
}
#endregion Functions - AD

#region Functions - Asset Report Project
Function Create-ReportSection
{
    #** This function is specific to this script and does all kinds of bad practice
    #   stuff. Use this function neither to learn from or judge me please. **
    #
    #   That being said, this function pretty much does all the report output
    #   options and layout magic. It depends upon the report layout hash and
    #   $HTMLRendering global variable hash.
    #
    #   This function generally shouldn't need to get changed in any way to customize your
    #   reports.
    #
    # .EXAMPLE
    #    Create-ReportSection -Rpt $ReportSection -Asset $Asset 
    #                         -Section 'Summary' -TableTitle 'System Summary'
    
    [CmdletBinding()]
    param(
        [parameter()]
        $Rpt,
        
        [parameter()]
        [string]$Asset,

        [parameter()]
        [string]$Section,
        
        [parameter()]
        [string]$TableTitle        
    )
    BEGIN
    {
        Add-Type -AssemblyName System.Web
    }
    PROCESS
    {}
    END
    {
        # Get our section type
        $RptSection = $Rpt['Sections'][$Section]
        $SectionType = $RptSection['Type']
        
        switch ($SectionType)
        {
            'Section'     # default to a data section
            {
                Write-Verbose -Message ('Create-ReportSection: {0}: {1}' -f $Asset,$Section)
                $ReportElementSource = @($RptSection['AllData'][$Asset])
                if ((($ReportElementSource.Count -gt 0) -and 
                     ($ReportElementSource[0] -ne $null)) -or 
                     ($RptSection['ShowSectionEvenWithNoData']))
                {
                    $SourceProperties = $RptSection['ReportTypes'][$ReportType]['Properties']
                    
                    #region report section type and layout
                    $TableType = $RptSection['ReportTypes'][$ReportType]['TableType']
                    $ContainerType = $RptSection['ReportTypes'][$ReportType]['ContainerType']

                    switch ($TableType)
                    {
                        'Horizontal' 
                        {
                            $PropertyCount = $SourceProperties.Count
                            $Vertical = $false
                        }
                        'Vertical' {
                            $PropertyCount = 2
                            $Vertical = $true
                        }
                        default {
                            if ((($SourceProperties.Count) -ge $HorizontalThreshold))
                            {
                                $PropertyCount = 2
                                $Vertical = $true
                            }
                            else
                            {
                                $PropertyCount = $SourceProperties.Count
                                $Vertical = $false
                            }
                        }
                    }
                    #endregion report section type and layout
                    
                    $Table = ''
                    If ($PropertyCount -ne 0)
                    {
                        # Create our future HTML table header
                        $SectionLink = '<a href="{0}"></a>' -f $Section
                        $TableHeader = $HTMLRendering['TableTitle'][$HTMLMode] -replace '<0>',$PropertyCount
                        $TableHeader = $SectionLink + ($TableHeader -replace '<1>',$TableTitle)

                        if ($RptSection.ContainsKey('Comment'))
                        {
                            if ($RptSection['Comment'] -ne $false)
                            {
                                $TableComment = $HTMLRendering['TableComment'][$HTMLMode] -replace '<0>',$PropertyCount
                                $TableComment = $TableComment -replace '<1>',$RptSection['Comment']
                                $TableHeader = $TableHeader + $TableComment
                            }
                        }
                        
                        $AllTableElements = @()
                        Foreach ($TableElement in $ReportElementSource)
                        {
                            $AllTableElements += $TableElement | Select $SourceProperties
                        }

                        # If we are creating a vertical table it takes a bit of transformational work
                        if ($Vertical)
                        {
                            $Count = 0
                            foreach ($Element in $AllTableElements)
                            {
                                $Count++
                                $SingleElement = [string]($Element | ConvertTo-PropertyValue | ConvertTo-Html)
                                if ($Rpt['Configuration']['PostProcessingEnabled'])
                                {
                                    # Add class elements for even/odd rows
                                    $SingleElement = Format-HTMLTable $SingleElement -ColorizeMethod 'ByEvenRows' -Attr 'class' -AttrValue 'even' -WholeRow
                                    $SingleElement = Format-HTMLTable $SingleElement -ColorizeMethod 'ByOddRows' -Attr 'class' -AttrValue 'odd' -WholeRow
                                    if ($RptSection.ContainsKey('PostProcessing') -and 
                                       ($RptSection['PostProcessing'].Value -ne $false))
                                    {
                                        $Rpt['Configuration']['PostProcessingEnabled'].Value
                                        $Table = $(Invoke-Command ([scriptblock]::Create($RptSection['PostProcessing'])))
                                    }
                                }
                                $SingleElement = [Regex]::Match($SingleElement, "(?s)(?<=</tr>)(.+)(?=</table>)").Value
                                $Table += $SingleElement 
                                if ($Count -ne $AllTableElements.Count)
                                {
                                    $Table += '<tr class="divide"><td></td><td></td></tr>'
                                }
                            }
                            $Table = '<table class="list">' + $TableHeader + $Table + '</table>'
                            $Table = [System.Web.HttpUtility]::HtmlDecode($Table)
                        }
                        # Otherwise it is a horizontal table
                        else
                        {
                            [string]$Table = $AllTableElements | ConvertTo-Html
                            if ($Rpt['Configuration']['PostProcessingEnabled'])
                            {
                                # Add class elements for even/odd rows
                                $Table = Format-HTMLTable $Table -ColorizeMethod 'ByEvenRows' -Attr 'class' -AttrValue 'even' -WholeRow
                                $Table = Format-HTMLTable $Table -ColorizeMethod 'ByOddRows' -Attr 'class' -AttrValue 'odd' -WholeRow
                                if ($RptSection.ContainsKey('PostProcessing'))
                                
                                {
                                    if ($RptSection.ContainsKey('PostProcessing'))
                                    {
                                        if ($RptSection['PostProcessing'] -ne $false)
                                        {
                                            $Table = $(Invoke-Command ([scriptblock]::Create($RptSection['PostProcessing'])))
                                        }
                                    }
                                }
                            }
                            # This will gank out everything after the first colgroup so we can replace it with our own spanned header
                            $Table = [Regex]::Match($Table, "(?s)(?<=</colgroup>)(.+)(?=</table>)").Value
                            $Table = '<table>' + $TableHeader + $Table + '</table>'
                            $Table = [System.Web.HttpUtility]::HtmlDecode(($Table))
                        }
                    }
                    
                    $Output = $HTMLRendering['SectionContainers'][$HTMLMode][$ContainerType]['Head'] + 
                              $Table + $HTMLRendering['SectionContainers'][$HTMLMode][$ContainerType]['Tail']
                    $Output
                }
            }
            'SectionBreak'
            {
                if ($Rpt['Configuration']['SkipSectionBreaks'] -eq $false)
                {
                    $Output = $HTMLRendering['CustomSections'][$SectionType] -replace '<0>',$TableTitle
                    $Output
                }
            }
        }
    }
}

Function Get-ADForestReportInformation
{
    [CmdletBinding()]
    param
    (
        [Parameter( HelpMessage="The custom report hash variable structure you plan to report upon")]
        $ReportContainer,
        [Parameter( HelpMessage="A sorted hash of enabled report elements.")]
        $SortedRpts
    )
    BEGIN
    {
        $verbose_timer = $verbose_starttime = Get-Date
        $ldapregex = [regex]'(?<LDAPType>^.+)\=(?<LDAPName>.+$)'
        try
        {
            $ADConnected = $true
            $schema = [DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
            $forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            $GCs = $forest.FindAllGlobalCatalogs()
            $GCNames = @($GCs | Select Name)
            $ForestDCs = @($forest.Domains | %{$_.DomainControllers} | Select Name)
            $ForestGCs = @((($GCs | Sort-Object -Property Name) | Select Name))
            $schemapartition = $schema.Name
            $RootDSC = [adsi]"LDAP://RootDSE"
            $DomNamingContext = $RootDSC.RootDomainNamingContext
            $ConfigNamingContext = $RootDSC.configurationNamingContext
            $Lync_ConfigPartition = 'None'

            $Path_LDAPPolicies = "LDAP://CN=Default Query Policy,CN=Query-Policies,CN=Directory Service,CN=Windows NT,CN=Services,$($ConfigNamingContext)"
            $Path_RecycleBinFeature = "LDAP://CN=Recycle Bin Feature,CN=Optional Features,CN=Directory Service,CN=Windows NT,CN=Services,$($ConfigNamingContext)"
            $Path_TombstoneLifetime = "LDAP://CN=Directory Service,CN=Windows NT,CN=Services,$($ConfigNamingContext)"
            $Path_ExchangeOrg = "LDAP://CN=Microsoft Exchange,CN=Services,$($ConfigNamingContext)"
            $Path_ExchangeVer = "LDAP://CN=ms-Exch-Schema-Version-Pt,$($SchemaPartition)"
            $Path_LyncVer = "LDAP://CN=ms-RTC-SIP-SchemaVersion,$($SchemaPartition)"
            $Path_ADSubnets = "LDAP://CN=Subnets,CN=Sites,$($ConfigNamingContext)"
            $Path_ADSiteLinks = "LDAP://CN=Sites,$($ConfigNamingContext)"
            
            $ExchangeFederations = @()
            $ExchangeServers = @()
            $Lync_Elements = @()
            $Sites = @()
            $SiteSubnets = @()
            $AllSiteConnections = @()
            $SiteLinks = @()
            $DomainControllers = @()
            $Domains = @()
            $DomainDFS = @()
            $DomainDFSR = @()
            $DomainTrusts = @()
            $DomainDNSZones = @()
            $DomainGPOs = @()
            $NPSServers = @()
            $DomainPrinters = @()
            $DomainPrivGroups = @()
        }
        catch
        {
            $ADConnected = $false
        }
    }
    PROCESS
    {}
    END
    {
        if ($ADConnected)
        {
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Forest Info - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
            #region Forest Settings
            # Recycle Bin Feature check
            $RecycleBinEnabled = $false
            if ([ADSI]::Exists($Path_RecycleBinFeature))
            {
                $RecycleBinAttribs = Search-AD -Properties * -SearchRoot $Path_RecycleBinFeature
                if ($RecycleBinAttribs.PSObject.Properties.Match('msDS-EnabledFeatureBL').Count) 
                {
                    $RecycleBinEnabled = $True
                }
            }

            if ([ADSI]::Exists($Path_TombstoneLifetime))
            {
                [ADSI]$TombstoneConfig = $Path_TombstoneLifetime
                $TombstoneLife = $TombstoneConfig.TombstoneLifetime
                $DeletedObjectLife = $TombstoneConfig."msDS-DeletedObjectLifetime"
                if ($TombstoneLife -ne $null)
                {
                    $TotalObjectBackupLife = $TombstoneLife
                }
                if ($deletedObjectLife) {
                    if (!$TombstoneLife -or 
                        ($DeletedObjectLife -lt $TombstoneLife)) 
                    {
                        $TotalObjectBackupLife = $deletedObjLifetime
                    }
                }
            }
            else
            {
                $TombstoneLife = 'NA'
                $DeletedObjectLife = 'NA'
                $TotalObjectBackupLife = 'NA'
            }
            if ([ADSI]::Exists($Path_LDAPPolicies))
            {
                [ADSI]$LDAPPoliciesConfig = $Path_LDAPPolicies
                $LDAPAdminLimits = $LDAPPoliciesConfig.LDAPAdminLimits
            }
            else
            {
                $LDAPAdminLimits = $null
            }
            #endregion Forest Settings
            
            #region DHCP Servers
            $DHCPServers = @(Search-AD -Filter '(objectclass=dHCPClass)' `
                                       -Properties Name,WhenCreated `
                                       -SearchRoot "LDAP://$([string]$ConfigNamingContext)" | 
                             Where {$_.Name -ne 'DhcpRoot'})
            #endregion DHCP Servers
            
            #region Exchange

            $ExchangeServerCount = 0
            if ([ADSI]::Exists($Path_ExchangeVer))
            {
                Write-Verbose -Message ('Get-ADForestReportInformation {0}: Exchange - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                [ADSI]$SchemaPathExchange = $Path_ExchangeVer
                $ExchangeSchema = ($SchemaPathExchange | Select rangeUpper).rangeUpper
                $ExchangeVersion = $SchemaHashExchange[$ExchangeSchema]
                $Props_ExchOrgs = @('distinguishedName',
                                    'Name')
                $Props_ExchServers = @('adspath',
                                       'Name',
                                       'msexchserversite',
                                       'msexchcurrentserverroles',
                                       'adminDisplayName',
                                       'whencreated',
                                       'serialnumber',
                                       'msexchproductid')
                $Props_ExchFeds = @('Name',
                                    'msExchFedIsEnabled',
                                    'msExchFedDomainNames',
                                    'msExchFedEnabledActions',
                                    'msExchFedTargetApplicationURI',
                                    'msExchFedTargetAutodiscoverEPR',
                                    'msExchVersion')

                if ([ADSI]::Exists($Path_ExchangeOrg))
                {
                    $ExchOrgs = @(Search-AD -Filter '(&(objectClass=msExchOrganizationContainer))' `
                                            -Properties $Props_ExchOrgs `
                                            -SearchRoot $Path_ExchangeOrg)
                    foreach ($ExchOrg in $ExchOrgs)
                    {
                        $ExchServers = @(Search-AD -Filter '(objectCategory=msExchExchangeServer)' `
                                                   -Properties $Props_ExchServers `
                                                   -SearchRoot "LDAP://$([string]$ExchOrg.distinguishedname)")
                        $ExchangeServerCount += $ExchServers.Count
                        foreach ($ExchServer in $ExchServers)
                        {
                            $AdminGroup = Get-ADPathName $ExchServer.adspath -GetElement 2 -ValuesOnly
                            $ExchSite =  Get-ADPathName $ExchServer.msexchserversite -GetElement 0 -ValuesOnly
                            $ExchRole = $ExchServer.msexchcurrentserverroles
                            # only have two roles in Exchange 2013 so we process a bit differently
                            if ($ExchServer.serialNumber -like "Version 15*")
                            {
                                switch ($ExchRole) {
                                    '54' {
                                        $ExchRole = 'MAILBOX'
                                    }
                                    '16385' {
                                        $ExchRole = 'CAS'
                                    }
                                    '16439' {
                                        $ExchRole = 'MAILBOX, CAS'
                                    }
                                }
                            }
                            else
                            {
                                if($ExchRole -ne 0)
                                {
                                    $ExchRole = [Enum]::Parse('MSExchCurrentServerRolesFlags', $ExchRole)
                                }
                            }
                            $exchserverprops = @{
                                Organization = $ExchOrg.Name
                                AdminGroup   = $AdminGroup
                                Name         = $ExchServer.adminDisplayName
                                Role         = $ExchRole
                                Site         = $ExchSite
                                Created      = $ExchServer.whencreated
                                Serial       = $ExchServer.serialnumber
                                ProductID    = $ExchServer.msexchproductid
                            }
                            $ExchangeServers += New-Object PSObject -Property $exchserverprops
                        }
                        $ExchangeFeds = @(Search-AD -Filter '(objectCategory=msExchFedSharingRelationship)' `
                                                   -Properties $Props_ExchFeds -DontJoinAttributeValues `
                                                   -SearchRoot "LDAP://CN=Federation,$([string]$ExchOrg.distinguishedname)")
                        Foreach ($ExchFed in $ExchangeFeds)
                        {
                            $ExchangeFedProps = @{
                                Organization = $ExchOrg.Name
                                Name = $ExchFed.Name
                                Enabled = $ExchFed.msExchFedIsEnabled
                                Domains = @($ExchFed.msExchFedDomainNames)
                                AllowedActions = @($ExchFed.msExchFedEnabledActions)
                                TargetAppURI = $ExchFed.msExchFedTargetApplicationURI
                                TargetAutodiscoverEPR = $ExchFed.msExchFedTargetAutodiscoverEPR
                                ExchangeVersion = $ExchFed.msExchVersion
                            }
                            $ExchangeFederations += New-Object psobject -Property $ExchangeFedProps
                        }
                    }
                }
            }
            else
            {
                $ExchangeVersion = 'Exchange Not Installed'
            }
            #endregion Exchange

            #region OCS/Lync
            $Lync_InternalServers = @()
            $Lync_EdgeServers = @()
            $Lync_Pools = @()
            $Lync_OtherServers = @()
            $LyncServerCount = 0
            $LyncPoolCount = 0
            if([ADSI]::Exists($Path_LyncVer))
            {
                Write-Verbose -Message ('Get-ADForestReportInformation {0}: Lync/OCS - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                # Get Lync version in forest
                [ADSI]$SchemaPathLync = $Path_LyncVer
                $LyncSchema = ($SchemaPathLync | Select rangeUpper).rangeUpper
                $LyncVersion = $SchemaHashLync[$LyncSchema]
                
                # Find Lync AD config partition location
                $LyncPathSearch = @(Search-AD -Filter '(objectclass=msRTCSIP-Service)' -SearchRoot "LDAP://$([string]$DomNamingContext)")
                if ($LyncPathSearch.count -ge 1)
                {
                    $OCSADContainer = ($LyncPathSearch[0]).adspath
                    $Lync_ConfigPartition = 'System'
                }
                else
                {
                    $LyncPathSearch = @(Search-AD -Filter '(objectclass=msRTCSIP-Service)' -SearchRoot "LDAP://$ConfigNamingContext")
                    if ($LyncPathSearch.count -ge 1)
                    {
                        $OCSADContainer = ($LyncPathSearch[0]).adspath
                        $Lync_ConfigPartition = 'Configuration'
                    }
                }
                
                # All internal Lync servers
                Search-AD -Filter '(&(objectClass=msRTCSIP-TrustedServer))' `
                          -Properties 'msrtcsip-trustedserverfqdn',Name `
                          -SearchRoot $OCSADContainer | 
                Sort-Object msrtcsip-trustedserverfqdn | %{ 
                    $LyncElementProps = @{
                        LyncElement = 'Server'
                        LyncElementType = 'Internal'
                        LyncElementName = $_.Name
                        LyncElementFQDN = $_.'msrtcsip-trustedserverfqdn'
                    }
                    $Lync_Elements += New-Object PSObject -Property $LyncElementProps
                }
                # All edge Lync servers
                Search-AD -Filter '(&(objectClass=msRTCSIP-EdgeProxy))' `
                          -Properties cn,Name,'msrtcsip-edgeproxyfqdn' `
                          -SearchRoot $OCSADContainer | 
                Sort-Object msrtcsip-edgeproxyfqdn | %{
                    $LyncElementProps = @{
                        LyncCN = $_.cn
                        LyncElement = 'Server'
                        LyncElementType = 'Edge'
                        LyncElementName = $_.Name
                        LyncElementFQDN = $_.'msrtcsip-edgeproxyfqdn'
                    }
                    $Lync_Elements += New-Object PSObject -Property $LyncElementProps
                }
                # All Lync global topology servers
                Search-AD -Filter '(&(objectClass=msRTCSIP-GlobalTopologySetting))' `
                          -Properties cn,Name,'msrtcsip-backendserver' `
                          -SearchRoot $OCSADContainer | 
                Sort-Object msrtcsip-backendserver | %{
                    $LyncElementProps = @{
                        LyncCN = $_.cn
                        LyncElement = 'Server'
                        LyncElementType = 'Backend'
                        LyncElementName = $_.Name
                        LyncElementFQDN = $_.'msrtcsip-backendserver'
                    }
                    $Lync_Elements += New-Object PSObject -Property $LyncElementProps
                }
                
                $LyncServerCount = $Lync_Elements.Count
                
                # All Lync pools
                $Lync_Pools = @(Search-AD -Filter '(&(objectClass=msRTCSIP-Pool))' `
                                          -Properties cn,dnshostname,'msrtcsip-pooldisplayname' `
                                          -SearchRoot $OCSADContainer | 
                                    Sort-Object dnshostname)
                $LyncPoolCount = $Lync_Pools.Count
                $Lync_Pools | %{
                    $LyncElementProps = @{
                        LyncCN = $_.cn
                        LyncElement = 'Pool'
                        LyncElementType = 'Pool'
                        LyncElementName = $_.'msrtcsip-pooldisplayname'
                        LyncElementFQDN = $_.dnshostname
                    }
                    $Lync_Elements += New-Object PSObject -Property $LyncElementProps
                }
            }
            else
            {
                $LyncSchema = $false
                $LyncVersion = 'Lync Not Installed'
            }
            #endregion OCS/Lync

            $ForestDataProps = @{
                ForestName = $forest.Name
                ForestFunctionalLevel = $forest.ForestMode
                SchemaMaster = $forest.SchemaRoleOwner
                DomainNamingMaster = $forest.NamingRoleOwner
                Sites = @(($forest.Sites | Sort-Object -Property Name | Select Name))
                Domains = @(($forest.Domains | Sort-Object -Property Name | Select Name))
                DomainControllers = $ForestDCs
                DomainControllersCount = $ForestDCs.Count
                GlobalCatalogs = $ForestGCs
                ExchangeServerCount = $ExchangeServerCount
                LyncADContainer = $Lync_ConfigPartition
                LyncServerCount = $LyncServerCount
                LyncPoolCount = $LyncPoolCount
                ExchangeVersion = [string]$ExchangeVersion
                ExchangeServers = $ExchangeServers
                LyncVersion = [string]$LyncVersion
                LyncElements = $Lync_Elements
                TombstoneLifetime = $TombstoneLife
                RecycleBinEnabled = $RecycleBinEnabled
                DeletedObjectLife = $DeletedObjectLife
                LDAPAdminLimits = $LDAPAdminLimits
            }
            $ForestData = New-Object psobject -Property $ForestDataProps
            
            #region AD site subnets
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Site Subnets - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
            $AD_SiteSubnets = @(Search-AD -Filter '(&(objectClass=subnet))' `
                                       -Properties name,location,siteobject `
                                       -SearchRoot $Path_ADSubnets | 
                                Sort-Object Name)
            Foreach ($Subnet in $AD_SiteSubnets)
            {
                if ($Subnet.siteobject -eq $null)
                {
                    $SiteName = ''
                }
                else
                {
                    $SiteName = Get-ADPathName $Subnet.siteobject -GetElement 0 -ValuesOnly
                }
                #$SiteName = [regex]::Match(($Subnet.siteobject).Split(',')[0], '(?<=CN=).+').Value
                $SiteSubnetProps = @{
                    'Name' = $Subnet.name
                    'Location' = $Subnet.location
                    'SiteName' = $SiteName
                }
                $SiteSubnets += New-Object PSObject -Property $SiteSubnetProps
            }
            #endregion AD site subnets

            #region AD Sites
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Sites - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
            $Prop_SitesExtended = @('Name',
                                    'DistinguishedName')
            $Prop_SiteConns = @('Name',
                                'DistinguishedName',
                                'Options',
                                'FromServer',
                                'EnabledConnection')
            $AD_SitesExtended = @(Search-AD -Filter '(&(objectClass=site))' `
                                 -Properties $Prop_SitesExtended `
                                 -SearchRoot "LDAP://CN=Sites,$([string]$ConfigNamingContext)")
            $AD_Sites = @([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites)

            ForEach($Site In $AD_Sites) 
            {            
                $SiteDN = [string]($AD_SitesExtended | Where {$_.Name -eq $Site.Name}).DistinguishedName
                $AD_SiteConnections = Search-AD -Filter '(&(objectClass=nTDSConnection))' `
                                                -Properties $Prop_SiteConns `
                                                -SearchRoot "LDAP://$SiteDN"
                $SiteConnections = @()
                if ($AD_SiteConnections -ne $null)
                {
                    Foreach ($SiteConnection in $AD_SiteConnections)
                    {
                        $tmpsiteconn = @($SiteConnection.Options)
                        If(($tmpsiteconn.Count -eq 0) -or ($SiteConnection.Options -eq 0) -or ($Site.Options -eq 'None'))
                        {
                            $SiteConnectionOptions = 'None'
                        }
                        Else
                        {
                            $SiteConnectionOptions = [Enum]::Parse('nTDSSiteConnectionSettingsFlags', $SiteConnection.Options)
                        }
                        
                        $FromServer = Get-ADPathName $SiteConnection.FromServer -GetElement 1 -ValuesOnly
                        $Server = Get-ADPathName $SiteConnection.distinguishedName -GetElement 2 -ValuesOnly

                        $SiteConnProps = @{
                            'DistinguishedName' = $SiteConnection.DistinguishedName
                            'Enabled' = $SiteConnection.EnabledConnection
                            'Options' = $SiteConnectionOptions
                            'FromServer' = $FromServer
                            'Server' = $Server
                        }
                        $SiteConnections += New-Object PSObject -Property $SiteConnProps
                        $AllSiteConnections += New-Object PSObject -Property $SiteConnProps
                    }
                }
                if (($Site.InterSiteTopologyGenerator -ne $null) -and ($Site.InterSiteTopologyGenerator -ne 'None'))
                {
                    $ISTGName = $Site.InterSiteTopologyGenerator | %{[string]$_.Name}
                }
                else
                {
                    $ISTGName = 'None'
                }
                $SiteProps = @{
                        'SiteName' = $Site.Name
                        #'DistinguishedName' = $DistinguishedName
                        'Domains' = @($Site.Domains | %{[string]$_.Name})
                        'Options' = $Site.Options
                        'Location' = $Site.Location
                        'ISTG' = $ISTGName
                        'SiteLinks' = @($Site.SiteLinks | %{[string]$_.Name})
                        'AdjacentSites' = @($Site.AdjacentSites | %{[string]$_.Name})
                        'BridgeheadServers' = ($Site.BridgeheadServers | %{[string]$_.Name})
                        'Connections' = $SiteConnections
                        'ConnectionCount' = $SiteConnections.Count
                        'Subnets' = @($Site.Subnets | %{[string]$_.Name})
                        'SubnetCount' = @($Site.Subnets).Count
                        'Servers' = @($Site.Servers | %{[string]$_.Name})
                        'ServerCount' = @($Site.Servers).Count
                }
                $Sites += New-Object PSObject -Property $SiteProps
            }
            #endregion AD Sites

            #region AD Site Links
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Site Links - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))

            $AD_SitesLinks = @(Search-AD -Filter '(&(objectClass=siteLink))' `
                                         -Properties cn,replInterval,siteList,options `
                                         -SearchRoot $Path_ADSiteLinks -DontJoinAttributeValues)

            Foreach ($SiteLink in $AD_SitesLinks)
            {
                $SitesInSiteLink = @()
                foreach ($Site in $SiteLink.siteList)
                {
                    $SiteName = Get-ADPathName $Site -GetElement 0 -ValuesOnly
                    $SitesInSiteLink += [string]$SiteName
                }
                $SiteLinkProp = @{
                    Name = $SiteLink.cn
                    repInterval = $SiteLink.replInterval
                    Sites = $SitesInSiteLink
                    ChangeNotification = ($SiteLink.options -eq 1)
                }
                $SiteLinks += new-object psobject -Property $SiteLinkProp
            }
            #endregion AD Site Links

            $SitesSummary = New-Object PSObject -Property @{
                'SiteCount' = $Sites.Count
                'SiteSubnetCount' = $SiteSubnets.Count
                'SiteLinkCount' = $SiteLinks.Count
                'SiteConnectionCount' = $AllSiteConnections.Count
                'SitesWithoutSiteConnections' = @($Sites | Where {$_.ConnectionCount -eq 0}).Count
                'SitesWithoutISTG' = @($Sites | Where {$_.ISTG -eq 'None'}).Count
                'SitesWithoutSubnets' = @($Sites | Where {$_.SubnetCount -eq 0}).Count
                'SitesWithoutServers' = @($Sites | Where {$_.ServerCount -eq 0}).Count
            }
            
            #region Domains
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Domains - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
            
            ForEach ($Dom in $Forest.Domains) 
            { 
                $CurDomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $Dom.Name)
                $DomainDN = 'dc=' + $Dom.Name.Replace('.', ',dc=')
                $NetBIOSName = Get-NETBiosName $DomainDN $ConfigNamingContext
                if ($Dom.Name -eq ($Forest.RootDomain).Name)
                {
                    $IsForestRoot = $True
                    $SchemaMaster = $forest.SchemaRoleOwner
                    $DomainNamingMaster = $forest.NamingRoleOwner
                }
                else
                {
                    $IsForestRoot = $False
                    $SchemaMaster = 'NA'
                    $DomainNamingMaster = 'NA'
                }
                try
                {
                    $CurDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($CurDomainContext)
                    $CurDomainDetails = [ADSI]"LDAP://$($CurDomain)"
                    $lngMaxPwdAge = $CurDomainDetails.ConvertLargeIntegerToInt64(($CurDomainDetails.maxPwdAge).Value)
                    $lngMinPwdAge = $CurDomainDetails.ConvertLargeIntegerToInt64(($CurDomainDetails.minPwdAge).Value)
                    
                    $DomainFunctionalLevel = $Dom.DomainMode
                    # RID Pool info
                    $Path_RIDManager = "LDAP://CN=RID Manager$,CN=System,$DomainDN"
                    $RIDInfo = Search-AD -Filter '(&(objectClass=rIDManager))' `
                                         -Properties rIDAvailablePool `
                                         -SearchRoot $Path_RIDManager
                    $RIDproperty = $RIDInfo.rIDAvailablePool
                    [int32]$totalSIDS = $($RIDproperty) / ([math]::Pow(2,32))
                    [int64]$temp64val = $totalSIDS * ([math]::Pow(2,32))
                    $RIDsIssued = [int32]($($RIDproperty) - $temp64val)
                    $RIDsRemaining = $totalSIDS - $RIDsIssued
                    $PDCEmulator = $Dom.PdcRoleOwner | select Name
                    $RIDMaster = $Dom.RidRoleOwner | select Name
                    $InfrastructureMaster = $Dom.InfrastructureRoleOwner | Select Name
                    $DomainDCs = @($Dom.DomainControllers | Select Name)
                    $lockoutThreshold = $CurDomainDetails.lockoutThreshold
                    $pwdHistoryLength = $CurDomainDetails.pwdHistoryLength
                    $minPwdLength = $CurDomainDetails.minPwdLength
                    $MaxPwdAge = -$lngMaxPwdAge/(600000000 * 1440)
                    $MinPwdAge = -$lngMinPwdAge/(600000000 * 1440)
                    $DomainAccessible = $true
                }
                catch
                {
                    Write-Warning ('Get-ADForestReportInformation: Issue with {0} Domain - {1}' -f $Dom.Name,$_.Exception.Message)
                    $DomainFunctionalLevel = 'NA'
                    $RIDsIssued = 0
                    $RIDsRemaining = 0
                    $PDCEmulator = 'NA'
                    $RIDMaster = 'NA'
                    $InfrastructureMaster = 'NA'
                    $DomainDCs = 'NA'
                    $lockoutThreshold = 0
                    $pwdHistoryLength = 0
                    $minPwdLength = 0
                    $MaxPwdAge = 0
                    $MinPwdAge = 0
                    $DomainAccessible = $false
                }
                $DomainProps = @{
                    DN = $DomainDN
                    Accessible = $DomainAccessible
                    Domain = $Dom.Name
                    NetBIOSName = $NetBIOSName
                    DomainFunctionalLevel = $DomainFunctionalLevel
                    IsForestRoot = $IsForestRoot
                    SchemaMaster = $SchemaMaster
                    DomainNamingMaster = $DomainNamingMaster
                    PDCEmulator = $PDCEmulator
                    RIDMaster = $RIDMaster
                    InfrastructureMaster = $InfrastructureMaster
                    DomainControllers = $DomainDCs
                    lockoutThreshold = $lockoutThreshold
                    pwdHistoryLength = $pwdHistoryLength
                    maxPwdAge = $MaxPwdAge
                    minPwdAge = $MinPwdAge
                    minPwdLength = $minPwdLength
                    RIDSIssued = $RIDsIssued
                    RIDSRemaining = $RIDsRemaining
                    #Sid = $DomSid
                }
                $Domains += New-Object psobject -Property $DomainProps
                if ($DomainAccessible)
                {
                    #region DCs
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: DCs - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    ForEach ($DC in $Dom.DomainControllers)
                    {
                        $IsGC = $false
                        $IsInfraMaster = $false
                        $IsNamingMaster = $false
                        $IsSchemaMaster = $false
                        $IsRidMaster = $false
                        $IsPdcMaster = $false
                        
                        if ($GCNames -match $DC.Name) { $IsGC = $true }
                        if ($DC.Roles -match 'RidRole') { $IsRidMaster = $true }
                        if ($DC.Roles -match 'PdcRole') { $IsPdcMaster = $true }
                        if ($DC.Roles -match 'InfrastructureRole') { $IsInfraMaster = $true }
                        if ($DC.Roles -match 'SchemaRole') { $IsSchemaMaster = $true }
                        if ($DC.Roles -match 'NamingRole') { $IsNamingMaster = $true }
                        $DCName = [string]$DC.Name
                        $DCName = $DCName.Split('.')[0]
                        $DCProps = @{
                            Forest = ($Dom.Forest).Name
                            Domain = $Dom.Name
                            Site = $DC.SiteName
                            Name = $DCName
                            OS = $DC.OSVersion
                            CurrentTime = $DC.CurrentTime
                            IPAddress = $DC.IPAddress
                          #  HighestUSN = $DC.HighestCommittedUsn
                            IsGC = $IsGC
                            IsInfraMaster = $IsInfraMaster
                            IsNamingMaster = $IsNamingMaster
                            IsSchemaMaster = $IsSchemaMaster
                            IsRidMaster = $IsRidMaster
                            IsPdcMaster = $IsPdcMaster
                        }
                        $DomainControllers += New-Object psobject -Property $DCProps
                    }
                    #endregion DCs
                 
                    #region DFS information
                    $Props_DFSItems = @( 'Name',
                                         'distinguishedName',
                                         'remoteServerName')
                    $Props_DFSGroupTopology = @( 'Name',
                                                 'distinguishedName',
                                                 'msDFSR-ComputerReference')
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: DFS - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $DFSDN = "CN=Dfs-Configuration,CN=System,$($DomainDN)"
                    $DFSItems = @(Search-AD -Filter '(&(objectClass=fTDfs))' `
                                            -Properties $Props_DFSItems `
                                            -SearchRoot "LDAP://$DFSDN")
                    foreach ($DFSItem in $DFSItems)
                    {
                        $DomDFSProps = @{
                            Domain = $Dom.Name
                            DN = $DFSItem.distinguishedName
                            Name = $DFSItem.Name
                            RemoteServerName = $DFSItem.remoteServerName -replace ('\*',"")
                        }
                        $DomainDFS += New-Object psobject -Property $DomDFSProps
                    }
                    #endregion DFS information
                    
                    #region DFSR information
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: DFSR - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $DFSRDN = "CN=DFSR-GlobalSettings,CN=System,$($DomainDN)"
                    $DFSRGroups = @(Search-AD -Filter '(&(objectClass=msDFSR-ReplicationGroup))' `
                                           -Properties Name,distinguishedName `
                                           -SearchRoot "LDAP://$($DFSRDN)")
                    foreach ($DFSRGroup in $DFSRGroups)
                    {
                        $DFSRGC = @()
                        $DFSRGTop = @()
                        $DFSRGroupContent = @(Search-AD -Filter '(&(objectClass=msDFSR-ContentSet))' `
                                           -Properties Name `
                                           -SearchRoot "LDAP://CN=Content,$($DFSRGroup.distinguishedName)")
                        $DFSRGroupTopology = @(Search-AD -Filter '(&(objectClass=msDFSR-Member))' `
                                           -Properties $Props_DFSGroupTopology `
                                           -SearchRoot "LDAP://CN=Topology,$($DFSRGroup.distinguishedName)")
                        $DFSRGC = @($DFSRGroupContent | %{$_.Name})
                        foreach ($DFSRGroupTopologyItem in $DFSRGroupTopology)
                        {
                            $DFSRServerName = Get-ADPathName $DFSRGroupTopologyItem.'msDFSR-ComputerReference' -GetElement 0 -ValuesOnly
                            $DFSRGTop += [string]$DFSRServerName
                        }
                        $DomDFSRProps = @{
                            Domain = $Dom.Name
                            Name = $DFSRGroup.Name
                            Content = $DFSRGC
                            RemoteServerName = $DFSRGTop
                        }
                        $DomainDFSR += New-Object psobject -Property $DomDFSRProps
                    }
                    #endregion DFSR information

                    #region AD Trusts
                    $ADProps_Trusts = @( 'trusttype',
                                         'trustattributes',
                                         'trustdirection',
                                         'flatname',
                                         'trustpartner',
                                         'whencreated',
                                         'whenchanged')
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: Trusts - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $TrustsDN = "CN=System,$($DomainDN)"
                    $AD_Trusts = @(Search-AD -Filter '(&(objectClass=trustedDomain))' `
                                             -SearchRoot "LDAP://$TrustsDN" `
                                             -Properties $ADProps_Trusts)
                    Foreach ($Trust in $AD_Trusts)
                    {
                        switch ($Trust.trusttype) 
                        {
                            1 { $TrustType = 'Downlevel (Windows NT)'}
                            2 { $TrustType = 'Uplevel (Active Directory)'}
                            3 { $TrustType = 'MIT (non-Windows)'}
                            4 { $TrustType = 'DCE (Theoretical)'}
                            default { $TrustType = $Trust.trusttype }
                        }
                        $TrustAttributes = [Enum]::Parse('MSTrustAttributeFlags', $Trust.trustattributes)
                        switch ($Trust.trustdirection)
                        {
                            1 { $TrustDirection = "Inbound"}
                            2 { $TrustDirection = "Outbound"}
                            3 { $TrustDirection = "Bidirectional"}
                            default { $TrustDirection = $Trust.trustdirection }
                        }
                        $TrustProps = @{
                            Domain = $Dom.Name
                            Name = $Trust.flatname
                            TrustedDomain = $Trust.trustpartner
                            Direction = $TrustDirection
                            Attributes = $TrustAttributes
                            TrustType = $TrustType
                            Created = $Trust.whencreated
                            Modified = $Trust.whenchanged
                        }
                        $DomainTrusts += New-Object PSObject -Property $TrustProps
                    }
                    #endregion AD Trusts
                    
                    #region AD Integrated DNS Zones
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: DNS Zones - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    # Pre-Windows 2003
                    $Path_DNSZoneDN = "LDAP://CN=MicrosoftDNS,CN=System,$DomainDN"
                    $AD_Zones = @(Search-AD -SearchRoot $Path_DNSZoneDN `
                                            -Filter '(objectclass=dnsZone)' `
                                            -Properties name,whencreated,whenchanged,distinguishedName)
                    if ($AD_Zones[0] -ne $null)
                    {
                        Foreach ($DNSZone in $AD_Zones)
                        {
                            $DNSEntryCount = @(Search-AD -SearchRoot "LDAP://$($DNSZone.distinguishedName)" `
                                                         -Filter '(objectclass=dnsNode)')
                            $DNSZoneProps = @{
                                Domain = $Dom.Name
                                AppPartition = 'Legacy'
                                Name  = $DNSZone.name
                                RecordCount = $DNSEntryCount.Count
                                Created = $DNSZone.whencreated
                                Changed = $DNSZone.whenchanged
                            }
                            $DomainDNSZones += New-Object psobject -Property $DNSZoneProps
                        }
                    }
                    $Path_DNSForestZoneDN = "LDAP://DC=ForestDnsZones,$DomainDN"
                    if ([ADSI]::Exists($Path_DNSForestZoneDN))
                    {
                        $AD_ForestZones = @(Search-AD -SearchRoot $Path_DNSForestZoneDN  `
                                                      -Filter '(objectclass=dnsZone)' `
                                                      -Properties name,whencreated,whenchanged,distinguishedName)
                        Foreach ($DNSZone in $AD_ForestZones)
                        {
                            $DNSEntryCount = @(Search-AD -SearchRoot "LDAP://$($DNSZone.distinguishedName)" `
                                                         -Filter '(objectclass=dnsNode)')
                            $DNSZoneProps = @{
                                Domain = $Dom.Name
                                AppPartition = 'Forest'
                                Name  = $DNSZone.name
                                RecordCount = $DNSEntryCount.Count
                                Created = $DNSZone.whencreated
                                Changed = $DNSZone.whenchanged
                            }
                            $DomainDNSZones += New-Object psobject -Property $DNSZoneProps
                        }
                    }
                    
                    $Path_DNSDomainZoneDN = "LDAP://DC=DomainDnsZones,$DomainDN"
                    if ([ADSI]::Exists($Path_DNSDomainZoneDN))
                    {
                        $AD_DomainZones = @(Search-AD -SearchRoot $Path_DNSDomainZoneDN `
                                                      -Filter '(objectclass=dnsZone)' `
                                                      -Properties name,whencreated,whenchanged,distinguishedName)
                        Foreach ($DNSZone in $AD_DomainZones)
                        {
                            $DNSEntryCount = @(Search-AD -SearchRoot "LDAP://$($DNSZone.distinguishedName)" `
                                                         -Filter '(objectclass=dnsNode)')
                            $DNSZoneProps = @{
                                Domain = $Dom.Name
                                AppPartition = 'Domain'
                                Name  = $DNSZone.name
                                RecordCount = $DNSEntryCount.Count
                                Created = $DNSZone.whencreated
                                Changed = $DNSZone.whenchanged
                            }
                            $DomainDNSZones += New-Object psobject -Property $DNSZoneProps
                        }
                    }
                    #endregion AD Integrated DNS Zones
                    
                    #region GPOs
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: GPOs - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $AD_DomainGPOs = @(Search-AD -SearchRoot "LDAP://$DomainDN" `
                                                 -Filter '(objectCategory=groupPolicyContainer)' `
                                                 -Properties displayname,whencreated,whenchanged)
                    Foreach ($GPO in $AD_DomainGPOs)
                    {
                        $DomainGPOProps = @{
                            Domain  = $Dom.Name
                            Name    = $GPO.displayname
                            Created = $GPO.whencreated
                            Changed = $GPO.whenchanged
                        }
                        $DomainGPOs += New-Object psobject -Property $DomainGPOProps
                    }
                    #endregion GPOs
                    
                    #region SMS Servers
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: Domain SMS Servers - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))

                    $SMSServers = @(Search-AD -Filter '(objectclass=mSSMSManagementPoint)' `
                                              -Properties dNSHostName,mSSMSSiteCode,mSSMSVersion,mSSMSDefaultMP,mSSMSDeviceManagementPoint `
                                              -SearchRoot "LDAP://$DomainDN" | 
                                    Select @{n='Domain';e={$Dom.Name}},dNSHostName,mSSMSSiteCode,mSSMSVersion,mSSMSDefaultMP,mSSMSDeviceManagementPoint)
                    #endregion SMS Servers

                    #region SMS Sites
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: Domain SMS Sites - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $SMSSites = @()
                    $SMSSiteDetails = @(Search-AD -Filter '(objectclass=mSSMSSite)' `
                                              -Properties Name,mSSMSSiteCode,mSSMSRoamingBoundaries `
                                              -SearchRoot "LDAP://$DomainDN" -DontJoinAttributeValues)
                    $SMSSiteDetails | Foreach {
                        $SMSSiteProps = @{
                            'Domain' = $Dom.Name
                            'Name' = $_.Name
                            'mSSMSSiteCode' = $_.mSSMSSiteCode
                            'mSSMSRoamingBoundaries' = @($_.mSSMSRoamingBoundaries)
                        }
                        $SMSSites += New-Object psobject -Property $SMSSiteProps
                    }
                    #endregion SMS Sites
                    
                    #region NPS Servers
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: Domain NPS Servers - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $NPSServers += @((Search-AD -SearchRoot "LDAP://$DomainDN" `
                                              -Filter "(ObjectCategory=group)(Name=RAS and IAS Servers)" `
                                              -Properties member -DontJoinAttributeValues).member | 
                                    Foreach {
                                        [adsi]"LDAP://$($_)" | select @{n='Domain';e={$Dom.Name}},
                                                                      @{n='Name';e={-join $_.name}},
                                                                      @{n='Type';e={$_.schemaclassname}}
                                    })
                    #endregion NPS Servers
                    
                    #region Printers
                    Write-Verbose -Message ('Get-ADForestReportInformation {0}: Domain Printers - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $DomainPrinters += @(Search-AD -SearchRoot "LDAP://$DomainDN" `
                                              -Filter "(objectCategory=printQueue)" `
                                              -Properties Name,ServerName,printShareName,location,drivername | 
                                        Select @{n='Domain';e={$Dom.Name}},Name,ServerName,printShareName,location,driverName)
                    #endregion Printers
                    #endregion Domains
                }
            }
            
            #region Populate Data
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Section Data - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
            $SortedRpts | %{ 
                switch ($_.Section) {
                    'ForestSummary' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($ForestData)
                    }
                    'SiteSummary' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($SitesSummary)
                    }
                    'ForestFeatures' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($ForestData)
                    }
                    'ForestDHCPServers' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DHCPServers)
                    }
                    'ForestExchangeInfo' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($ForestData.ExchangeServers)
                    }
                    'ForestExchangeFederations' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($ExchangeFederations)
                    }
                    'ForestLyncInfo' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($ForestData.LyncElements)
                    }
                    'ForestSiteSummary' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($Sites)
                    }
                    'ForestSiteDetails' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($Sites)
                    }
                    'ForestSiteSubnets' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($SiteSubnets)
                    }
                    'ForestSiteConnections' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($AllSiteConnections)
                    }
                    'ForestSiteLinks' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($SiteLinks)
                    }
                    'ForestDomains' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($Domains)
                    }
                    'ForestDomainDCs' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainControllers)
                    }
                    'ForestDomainPasswordPolicy' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($Domains)
                    }
                    'ForestDomainTrusts' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainTrusts)
                    }
                    'ForestDomainDFSShares' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainDFS)
                    }
                    'ForestDomainDFSRShares' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainDFSR)
                    }
                    'ForestDomainDNSZones' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainDNSZones)
                    }
                    'ForestDomainGPOs' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainGPOs)
                    }
                    'ForestDomainNPSServers' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($NPSServers)
                    }
                    'ForestDomainSCCMServers' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($SMSServers)
                    }
                    'ForestDomainSCCMSites' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($SMSSites)
                    }
                    'ForestDomainPrinters' {
                        $ReportContainer['Sections'][$_]['AllData'][$ForestData.ForestName] = 
                            @($DomainPrinters)
                    }
                }
            }
            #endregion Populate Data
            
            #region Create Diagrams
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Diagrams - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
            # Replication Connection diagram
            $ReplicationDiagram = @'
digraph test {
 rankdir = LR
 
'@
            ForEach ($Site in $Sites)
            {
                ForEach ($ReplCon in $Site.Connections)
                {
                    $ReplicationDiagram += @"    
            
 "$($ReplCon.FromServer)" -> "$($ReplCon.Server)"[label = "Replicates To"]
"@
               }
            }
            $ReplicationDiagram += @'

}
'@
            If ($AD_CreateDiagramSourceFiles)
            {
                $ReplicationDiagram | Out-File -Encoding ASCII '.\ReplicationDiagram.txt'
            }
            If ($AD_CreateDiagrams)
            {
                
                $ReplicationDiagram | & "$($Graphviz_Path)dot.exe" -Tpng -o ReplicationDiagram.png
            }
            
            # Domain Trust Connection diagram
            $TrustDiagram = @'
digraph test {
 rankdir = LR
 
'@
            ForEach ($Trust in $DomainTrusts)
            {
                $TrustDiagram += @"

 "$($Trust.Domain)" -> "$($Trust.TrustedDomain)"[label = "Trusts"]
"@
            }

            $TrustDiagram += @'

}
'@
            If ($AD_CreateDiagramSourceFiles)
            {
                $TrustDiagram | Out-File -Encoding ASCII '.\DomainTrustDiagram.txt'
            }
            If ($AD_CreateDiagrams)
            {
                $TrustDiagram | & "$($Graphviz_Path)dot.exe" -Tpng -o DomainTrustDiagram.png
            }
            
            # Site Adjacency Diagram
            $SiteAdjacencyDiagram = @'
digraph test {
 rankdir = LR
 
'@
            ForEach ($Site in $Sites)
            {
                Foreach ($AdjSite in $Site.AdjacentSites)
                {
                    $SiteAdjacencyDiagram += @"    
        
     "$($Site.SiteName)" -> "$($AdjSite)"[label = "Adjacent To"]
"@
                }
            }

            $SiteAdjacencyDiagram += @'

}
'@
            If ($AD_CreateDiagramSourceFiles)
            {
                $SiteAdjacencyDiagram | Out-File -Encoding ASCII '.\SiteAdjDiagram.txt'
            }
            If ($AD_CreateDiagrams)
            {
                $SiteAdjacencyDiagram | & "$($Graphviz_Path)dot.exe" -Tpng -o SiteAdjDiagram.png
            }
            #endregion Create Diagrams
            
            $ReportContainer['Configuration']['Assets'] = $ForestData.ForestName
            Return $ForestData.ForestName
            Write-Verbose -Message ('Get-ADForestReportInformation {0}: Finished - {1}' -f $forest.Name,$((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
        }
    }
}

Function Get-ADDomainReportInformation
{
    [CmdletBinding()]
    param
    (
        [Parameter( HelpMessage="The custom report hash variable structure you plan to report upon")]
        $ReportContainer,
        [Parameter( HelpMessage="A sorted hash of enabled report elements.")]
        $SortedRpts
    )
    BEGIN
    {
        try
        {
            $verbose_timer = Get-Date
            $Filter_Users = '(samAccountType=805306368)'
            $Filter_User_Locked = '(samAccountType=805306368)(lockoutTime:1.2.840.113556.1.4.804:=4294967295)'
            $Filter_User_PasswordChangeReq = '(samAccountType=805306368)(pwdLastSet=0)(!useraccountcontrol:1.2.840.113556.1.4.803:=2)'
            $Filter_User_Enabled = '(samAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))'
            $Filter_User_Disabled = '(samAccountType=805306368)(useraccountcontrol:1.2.840.113556.1.4.803:=2)'
            $Filter_User_NoPasswordReq = '(samAccountType=805306368)(UserAccountControl:1.2.840.113556.1.4.803:=32)'
            $Filter_User_PasswordNeverExpires = '(samAccountType=805306368)(UserAccountControl:1.2.840.113556.1.4.803:=65536)'
            $Filter_User_DialinEnabled = '(samAccountType=805306368)(msNPAllowDialin=TRUE)'
            $Filter_User_UnconstrainedDelegation = '(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=524288)'
            $Filter_User_NotTrustedForDelegation = '(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=524288)'
            $Filter_User_NoPreauth = '(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304)'
            $Filter_User_ControlAccessWithNPS = '(samAccountType=805306368)(!(msNPAllowDialin=*))'

            $RootDSC = [adsi]"LDAP://RootDSE"
            $DomNamingContext = $RootDSC.RootDomainNamingContext
            $ConfigNamingContext = $RootDSC.configurationNamingContext
            $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            $Domains = @($Forest.Domains | %{[string]$_.Name})
            
            $ADConnected = $true
        }
        catch
        {
            $ADConnected = $false
        }
    }
    PROCESS
    {}
    END
    {
        if ($ADConnected)
            {
            Foreach ($Dom in $Domains)
            {
                $CurDomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $Dom)
                try
                {
                    $CurDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($CurDomainContext)
                    Write-Verbose -Message ('Get-ADDomainReportInformation: Start - {0}' -f $verbose_timer)
                    Write-Verbose -Message ('Get-ADDomainReportInformation: Domain - {0}' -f $Dom)
                    $UserStats = $null
                    $GroupStats = $null
                    $PrivGroups = $null
                    $PrivGroupMembers = $null
                    $TotalPrivGroupCount = 0
                    
                    $DomainDN = 'dc=' + $Dom.Replace('.', ',dc=')
                    $Splat_SearchAD = @{
                        'SearchRoot' = "LDAP://$DomainDN"
                        'Properties' = $UserAttribs
                    }
                    if ($EXPORTTOCSV_ALLUSERS)
                    {
                        Write-Verbose -Message ('Get-ADDomainReportInformation: Export all users in domain - {0}' -f $Dom)
                        Search-AD -Properties $UserAttribs `
                                  -Filter '(samAccountType=805306368)' `
                                  -SearchRoot "LDAP://$DomainDN" |
                            Normalize-ADUsers -Attribs $UserAttribs | 
                                Append-ADUserAccountControl |
                                    Export-Csv -NoTypeInformation "allusers_$Dom.csv"
                        Write-Verbose -Message ('Get-ADDomainReportInformation: Timer - {0}' -f $((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    }
                    Write-Verbose -Message ('Get-ADDomainReportInformation: Domain User Stats - {0}' -f $Dom)
                    $UserStats = New-Object psobject -Property @{
                        'Total' = @(Search-AD @Splat_SearchAD -Filter $Filter_Users).Count
                        'Enabled' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_Enabled).Count
                        'Disabled' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_Disabled).Count
                        'Locked' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_Locked).Count
                        'PwdDoesNotExpire' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_PasswordNeverExpires).Count
                        'PwdNotRequired' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_NoPasswordReq).Count
                        'PwdMustChange' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_PasswordChangeReq).Count
                        'DialInEnabled' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_DialinEnabled).Count
                        'UnconstrainedDelegation' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_UnconstrainedDelegation).Count
                        'NotTrustedForDelegation' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_NotTrustedForDelegation).Count
                        'NoPreAuthRequired' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_NoPreauth).Count
                        'ControlAccessWithNPS' = @(Search-AD @Splat_SearchAD -Filter $Filter_User_ControlAccessWithNPS).Count
                    }
                    Write-Verbose -Message ('Get-ADDomainReportInformation: Timer - {0}' -f $((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    $AllGroups = @(
                        Search-AD -Properties groupType `
                                  -Filter '(objectClass=group)' `
                                  -SearchRoot "LDAP://$DomainDN"
                    )
                    Write-Verbose -Message ('Get-ADDomainReportInformation: Domain Group Stats - {0}' -f $Dom)
                    $GroupStats = New-Object psobject -Property @{
                        'Total' = $AllGroups.Count
                        'Builtin' = @($AllGroups | Where {$_.groupType -eq '-2147483643'}).Count
                        'UniversalSecurity' = @($AllGroups | Where {$_.groupType -eq '-2147483640'}).Count
                        'UniversalDist' = @($AllGroups | Where {$_.groupType -eq '8'}).Count
                        'GlobalSecurity' = @($AllGroups | Where {$_.groupType -eq '-2147483646'}).Count
                        'GlobalDist' = @($AllGroups | Where {$_.groupType -eq '2'}).Count
                        'DomainLocalSecurity' = @($AllGroups | Where {$_.groupType -eq '-2147483644'}).Count
                        'DomainLocalDist' = @($AllGroups | Where {$_.groupType -eq '4'}).Count
                    }
                    $PrivGroups = @(Get-ADPrivilegedGroups -Domain $Dom)
                    $PrivUsers = @(Get-ADDomainPrivAccounts -Domain $Dom)
                    Write-Verbose -Message ('Get-ADDomainReportInformation: Timer - {0}' -f $((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    if ($EXPORTTOCSV_PRIVUSERS)
                    {
                        Write-Verbose -Message ('Get-ADDomainReportInformation: Exporting privileged users - {0}' -f $Dom)
                        $PrivUsers | Export-Csv -NoTypeInformation "privusers_$Dom.csv"
                        Write-Verbose -Message ('Get-ADDomainReportInformation: Timer - {0}' -f $((New-TimeSpan $verbose_timer ($verbose_timer = get-date)).totalseconds))
                    }
                    $PrivGroupStats = @()
                    
                    ForEach ($PrivGroup in $PrivGroups)
                    {
                        Foreach ($PrivGrp in $AD_PrivilegedGroups)
                        {
                            if ($PrivGrp -eq $PrivGroup.Group)
                            {
                                $PrivGroupCount = @($PrivUsers | Where {$_.PrivGroup -eq $PrivGrp}).Count
                                $TotalPrivGroupCount = $TotalPrivGroupCount + $PrivGroupCount
                                $PrivGroupStatProp = @{
                                    AdminGroup =  $PrivGrp
                                    DisplayName = $PrivGroup.GroupName
                                    MemberCount = $PrivGroupCount
                                }
                                $PrivGroupStats += New-Object psobject -Property $PrivGroupStatProp
                            }
                        }
                    }
                    #region Populate Data
                    $SortedRpts | %{ 
                        switch ($_.Section) {
                            'UserAccountStats1' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($UserStats)
                            }
                            'UserAccountStats2' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($UserStats)
                            }
                            'GroupStats' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($GroupStats)
                            }
                            'PrivGroupStats' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivGroupStats)
                            }
                            'PrivGroup_EnterpriseAdmins' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Enterprise Admins'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_SchemaAdmins' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Schema Admins'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_DomainAdmins' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Domain Admins'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_Administrators' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Administrators'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_AccountOperators' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Account Operators'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_ServerOperators' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Server Operators'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_BackupOperators' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Backup Operators'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_PrintOperators' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Print Operators'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                            'PrivGroup_CertPublishers' {
                                $ReportContainer['Sections'][$_]['AllData'][$Dom] = 
                                    @($PrivUsers | 
                                      Where {$_.PrivGroup -eq 'Cert Publishers'} |
                                      Sort-Object -Property PasswordAge -Descending)
                            }
                        }
                    }
                    #endregion Populate Data
                }
                catch
                {
                    Write-Warning ('Get-ADForestReportInformation: Issue with {0} Domain - {1}' -f $Dom,$_.Exception.Message)
                }
            }
            $ReportContainer['Configuration']['Assets'] = $Domains
            Return $Domains
        }
    }
}

Function New-ReportDelivery
{
    [CmdletBinding()]
    param
    (
        [Parameter( HelpMessage="Report body, typically in HTML format", ValueFromPipeline=$true )]
        [string[]]
        $Report,
        
        [Parameter( ParameterSetName="EmailReport", HelpMessage="Send email of resulting report?")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]                    
        [switch]
        $SendMail,
        
        [Parameter( ParameterSetName="EmailReport", HelpMessage="Email server to relay report through")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [string]
        $EmailRelay = ".",
        
        [Parameter( ParameterSetName="EmailReport", HelpMessage="Email sender")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [string]
        $EmailSender='systemreport@localhost',
        
        [Parameter( ParameterSetName="EmailReport", Mandatory=$true, HelpMessage="Email recipient")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [string]
        $EmailRecipient,
        
        [Parameter( ParameterSetName="EmailReport", HelpMessage="Email subject")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [string]
        $EmailSubject='System Report',
        
        [Parameter( ParameterSetName="EmailReport", HelpMessage="Email report(s) as attachement")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [Parameter( ParameterSetName = "EmailReportAsAttachment")]
        [switch]
        $EmailAsAttachment,
        
        [Parameter( ParameterSetName="EmailReport", HelpMessage="Force email to be sent anonymously?")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [switch]
        $ForceAnonymous,

        [Parameter( ParameterSetName="SaveReport", HelpMessage="Save the report?")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [switch]
        $SaveReport,
        
        [Parameter( ParameterSetName="SaveReport", HelpMessage="Zip the report(s).")]
        [Parameter( ParameterSetName = "EmailAndSaveReport")]
        [Parameter( ParameterSetName = "EmailReportAsAttachment")]
        [switch]
        $ZipReport
    )
    BEGIN
    {
        $Reports = @()      # Save a list of report paths in case we will be emailing as attachments
        if ($SaveReport)
        {
            $ReportFormat = 'HTML'
        }
        if ($SaveAsPDF)
        {
            $PdfGenerator = "$((Get-Location).Path)\NReco.PdfGenerator.dll"
            if (Test-Path $PdfGenerator)
            {
                $ReportFormat = 'PDF'
                $PdfGenerator = "$((Get-Location).Path)\NReco.PdfGenerator.dll"
                $Assembly = [Reflection.Assembly]::LoadFrom($PdfGenerator) #| Out-Null
                $PdfCreator = New-Object NReco.PdfGenerator.HtmlToPdfConverter
            }
        }
    }
    PROCESS
    {
        switch ($ReportFormat) {
            'PDF' {
                $ReportOutput = $PdfCreator.GeneratePdf([string]$Report)
                $ReportName = $ReportName -replace '.html','.pdf'
                Add-Content -Value $ReportOutput `
                            -Encoding byte `
                            -Path ($ReportName)
            }
            'HTML' {
                $Report | Out-File $ReportName
            }
        }
        $Reports += $ReportName
    }
    END
    {
        if ($Sendmail)
        {
            $SendMailSplat = @{
                'From' = $EmailSender
                'To' = $EmailRecipient
                'Subject' = $EmailSubject
                'Priority' = 'Normal'
                'smtpServer' = $EmailRelay
                'BodyAsHTML' = $true
            }
            if ($ForceAnonymous)
            {
                $Pass = ConvertTo-SecureString –String 'anonymous' –AsPlainText -Force
                $Creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "NT AUTHORITY\ANONYMOUS LOGON", $pass
                $SendMailSplat.Credential = $creds

            }
            if ($EmailAsAttachment)
            {
                if ($ZipReport)
                {
                    $ZipName = $ReportName -replace '.html','.zip'
                    $Reports | New-ZipFile -ZipFilePath $ZipName -Append
                }
                else
                {
                    $SendMailSplat.Attachments = $Reports
                }
            }
            else
            {
                $SendMailSplat.Body = $Report
            }
            send-mailmessage @SendMailSplat
        }
    }
}

Function New-ReportOutput
{
    [CmdletBinding()]
    param
    (
        [Parameter( HelpMessage="Report body, typically in HTML format",
                    ValueFromPipeline=$true,
                    Mandatory=$true )]
        [string]
        $Report,
        
        [Parameter( HelpMessage="Save the report as a PDF. If the PDF library is not available the default format, HTML, will be used instead.")]
        [switch]
        $SaveAsPDF,
        
        [Parameter( HelpMessage="Postpend timestamp to file name.")]
        [switch]
        $Postpendtimestamp,
        
        [Parameter( HelpMessage="Prepend timestamp to file name.")]
        [switch]
        $Prependtimestamp,
        
        [Parameter( HelpMessage="If output already exists do not overwrite.")]
        [switch]
        $NoOverwrite,
        
        [Parameter( HelpMessage="If saving the report, what do you want to call it?")]
        [string]
        $ReportName="Report.html",
        
        [Parameter( HelpMessage="Where are you saving the report (defaults to local temp directory)?")]
        [string]
        $ReportPath=$env:Temp
    )
    BEGIN
    {
        $timestamp = Get-Date -Format ddmmyyyy-HHMMss
        if ($Prependtimestamp)
        {
            $ReportName="$timestamp_$($ReportName.Split('.')[0]).$($ReportName.Split('.')[1])"
        }
        if ($Postpendtimestamp)
        {
            $ReportName="$($ReportName.Split('.')[0])_$timestamp.$($ReportName.Split('.')[1])"
        }
        $ReportFormat = 'HTML'
        if ($SaveAsPDF)
        {
            $PdfGenerator = "$((Get-Location).Path)\NReco.PdfGenerator.dll"
            if (Test-Path $PdfGenerator)
            {
                try {
                    $ReportFormat = 'PDF'
                    $PdfGenerator = "$((Get-Location).Path)\NReco.PdfGenerator.dll"
                    $Assembly = [Reflection.Assembly]::LoadFrom($PdfGenerator) #| Out-Null
                    $PdfCreator = New-Object NReco.PdfGenerator.HtmlToPdfConverter
                }
                catch {
                    $ReportFormat = 'HTML'
                }
            }
        }
    }
    PROCESS
    {}
    END
    {
        switch ($ReportFormat) {
            'PDF' {
                $ReportOutput = $PdfCreator.GeneratePdf([string]$Report)
                if ($ReportName -notmatch "\.pdf$") 
                {
                    if ($ReportName -match "\.html{0,1}$") 
                    {
                        $ReportName = [System.Text.RegularExpressions.Regex]::Replace($ReportName,"\.html{0,1}$", '.pdf');
                    }
                    else
                    {
                        $ReportName = "$($ReportName).pdf"
                    }
                }
                if ((Test-Path "$ReportPath\$ReportName") -and $NoOverwrite)
                {
                    $retval = $false
                }
                else
                {
                    Add-Content -Value $ReportOutput `
                                -Encoding byte `
                                -Path ("$ReportPath\$ReportName")
                    $retval = "$ReportPath\$ReportName"
                }
            }
            'HTML' {
                if ($ReportName -notmatch "\.html{0,1}$")
                {
                    if ($ReportName -match "\.pdf$") 
                    {
                        $ReportName = [System.Text.RegularExpressions.Regex]::Replace($ReportName,"\.pdf$", '.html');
                    }
                    else
                    {
                        $ReportName = "$($ReportName).html"
                    }
                }
                if ((Test-Path "$ReportPath\$ReportName") -and $NoOverwrite)
                {
                    $retval = $false
                }
                else
                {
                    $Report | Out-File "$ReportPath\$ReportName"
                    $retval = "$ReportPath\$ReportName"
                }
            }
        }
        return $retval
    }
}

Function New-SelfContainedAssetReport
{
    <#
    .SYNOPSIS
        Generates a new asset report from gathered data.
    .DESCRIPTION
        Generates a new asset report from gathered data. The information 
        gathering routine generates the output root elements.
    .PARAMETER ReportContainer
        The custom report hash vaiable structure you plan to report upon.
    .PARAMETER DontGatherData
        If your report container already has all the data from a prior run and
        you are just creating a different kind of report with the same data, enable this switch
    .PARAMETER ReportType
        The report type.
    .PARAMETER HTMLMode
        The HTML rendering type (DynamicGrid or EmailFriendly).
    .PARAMETER ExportToExcel
        Export an excel document.
    .PARAMETER EmailRelay
        Email server to relay report through.
    .PARAMETER EmailSender
        Email sender.
    .PARAMETER EmailRecipient
        Email recipient.
    .PARAMETER EmailSubject
        Email subject.
    .PARAMETER SendMail
        Send email of resulting report?
    .PARAMETER ForceAnonymous
        Force email to be sent anonymously?
    .PARAMETER SaveReport
        Save the report?
    .PARAMETER SaveAsPDF
        Save the report as a PDF. If the PDF library is not available the default format, HTML, will be used instead.
    .PARAMETER OutputMethod
        If saving the report, will it be one big report or individual reports?
    .PARAMETER ReportName
        If saving the report, what do you want to call it? This is only used if one big report is being generated.
    .PARAMETER ReportNamePrefix
        Prepend an optional prefix to the report name?
    .PARAMETER ReportLocation
        If saving multiple reports, where will they be saved?
    .EXAMPLE
        New-SelfContainedAssetReport -ReportContainer $ADForestReport -ExportToExcel `
            -SaveReport `
            -OutputMethod 'IndividualReport' `
            -HTMLMode 'DynamicGrid'

        Description:
        ------------------
        Create a forest active directory report.
    .NOTES
        Version    : 1.0.0 10/15/2013
                     - First release

        Author     : Zachary Loeber

        Disclaimer : This script is provided AS IS without warranty of any kind. I 
                     disclaim all implied warranties including, without limitation,
                     any implied warranties of merchantability or of fitness for a 
                     particular purpose. The entire risk arising out of the use or
                     performance of the sample scripts and documentation remains
                     with you. In no event shall I be liable for any damages 
                     whatsoever (including, without limitation, damages for loss of 
                     business profits, business interruption, loss of business 
                     information, or other pecuniary loss) arising out of the use of or 
                     inability to use the script or documentation. 

        Copyright  : I believe in sharing knowledge, so this script and its use is 
                     subject to : http://creativecommons.org/licenses/by-sa/3.0/
    .LINK
        http://www.the-little-things.net/
    .LINK
        http://nl.linkedin.com/in/zloeber

    #>

    #region Parameters
    [CmdletBinding()]
    PARAM
    (
        [Parameter(Mandatory=$true,
                   HelpMessage='The custom report hash variable structure you plan to report upon')]
        $ReportContainer,
        
        [Parameter(HelpMessage='Do not gather data, this assumes $Reportcontainer has been pre-populated.')]
        [switch]
        $DontGatherData,
        
        [Parameter( HelpMessage='The report type')]
        [string]
        $ReportType = '',
        
        [Parameter( HelpMessage='The HTML rendering type (DynamicGrid or EmailFriendly)')]
        [ValidateSet('DynamicGrid','EmailFriendly')]
        [string]
        $HTMLMode = 'DynamicGrid',
        
        [Parameter( HelpMessage='Export an excel document as part of the output')]
        [switch]
        $ExportToExcel,
        
        [Parameter( HelpMessage='Skip html/pdf generation, only produce an excel report (if switch is enabled)')]
        [switch]
        $NoReport,
        
        [Parameter( HelpMessage='Email server to relay report through')]
        [string]
        $EmailRelay = '.',
        
        [Parameter( HelpMessage='Email sender')]
        [string]
        $EmailSender='systemreport@localhost',
     
        [Parameter( HelpMessage='Email recipient')]
        [string]
        $EmailRecipient='default@yourdomain.com',
        
        [Parameter( HelpMessage='Email subject')]
        [string]
        $EmailSubject='System Report',
        
        [Parameter( HelpMessage='Send email of resulting report?')]
        [switch]
        $SendMail,
        
        [Parameter( HelpMessage="Force email to be sent anonymously?")]
        [switch]
        $ForceAnonymous,

        [Parameter( HelpMessage='Save the report?')]
        [switch]
        $SaveReport,
        
        [Parameter( HelpMessage='Save the data gathered for later processing?')]
        [switch]
        $SaveData,
        
        [Parameter( HelpMessage='Save the data gathered for later processing?')]
        [string]
        $SaveDataFile='DataFile.xml',
        
        [Parameter( HelpMessage='Skip information gathering?')]
        [switch]
        $SkipInformationGathering,
        
        [Parameter( HelpMessage='Save the report as a PDF. If the PDF library is not available the default format, HTML, will be used instead.')]
        [switch]
        $SaveAsPDF,

        [Parameter( HelpMessage='Zip up the report(s)?')]
        [switch]
        $ZipReport,
       
        [Parameter( HelpMessage='How to process report output?')]
        [ValidateSet('OneBigReport','IndividualReport','NoReport')]
        [string]
        $OutputMethod='OneBigReport',
        
        [Parameter( HelpMessage='If saving the report, what do you want to call it?')]
        [string]
        $ReportName='Report.html',
        
        [Parameter( HelpMessage='Prepend an optional prefix to the report name?')]
        [string]
        $ReportNamePrefix='',
        
        [Parameter( HelpMessage='If saving multiple reports, where will they be saved?')]
        [string]
        $ReportLocation='.'
    )
    #endregion Parameters
    BEGIN
    {
        # Use this to keep a splat of our CmdletBinding options
        $VerboseDebug=@{}
        If ($PSBoundParameters.ContainsKey('Verbose')) 
        {
            If ($PSBoundParameters.Verbose -eq $true)
            {
                $VerboseDebug.Verbose = $true
            } 
            else 
            {
                $VerboseDebug.Verbose = $false
            }
        }
        If ($PSBoundParameters.ContainsKey('Debug')) 
        {
            If ($PSBoundParameters.Debug -eq $true)
            {
                $VerboseDebug.Debug = $true 
            } 
            else 
            {
                $VerboseDebug.Debug = $false
            }
        }

        $ReportOutputSplat = @{
            'SaveAsPDF' = $SaveAsPDF
        }
        
        # Some basic initialization
        $AssetReports = ''
        $FinishedReportPaths = @()
        
        if (($ReportType -eq '') -or ($ReportContainer['Configuration']['ReportTypes'] -notcontains $ReportType))
        {
            $ReportType = $ReportContainer['Configuration']['ReportTypes'][0]
        }
        # There must be a more elegant way to do this hash sorting but this also allows
        # us to pull a list of only the sections which are defined and need to be generated.d
        $SortedReports = @()
        Foreach ($Key in $ReportContainer['Sections'].Keys) 
        {
            if ($ReportContainer['Sections'][$Key]['ReportTypes'].ContainsKey($ReportType))
            {
                if ($ReportContainer['Sections'][$Key]['Enabled'] -and 
                    ($ReportContainer['Sections'][$Key]['ReportTypes'][$ReportType] -ne $false))
                {
                    $_SortedReportProp = @{
                                            'Section' = $Key
                                            'Order' = $ReportContainer['Sections'][$Key]['Order']
                                          }
                    $SortedReports += New-Object -Type PSObject -Property $_SortedReportProp
                }
            }
        }
        $SortedReports = $SortedReports | Sort-Object Order
    }
    PROCESS
    {}
    END 
    {
        if ($SkipInformationGathering)
        {
            $AssetNames = @($ReportContainer['Configuration']['Assets'])
        }
        else
        {
            # Information Gathering, Your custom script block must return the 
            #   array of strings (keys) which consist of the Root elements of your
            #   desired reports.
            Write-Verbose -Message ('New-SelfContainedAssetReport: Invoking information gathering script...')
            $AssetNames = 
                @(Invoke-Command ([scriptblock]::Create($ReportContainer['Configuration']['PreProcessing'])))
        }
        if ($AssetNames.Count -ge 1)
        {
            if ($SaveData)
            {
                $ReportContainer | Export-CliXml -Path ($ReportNamePrefix + $SaveDataFile)
            }
            # if we are to export all data to excel, then we do so per section
            #   then per Asset
            if ($ExportToExcel)
            {
                Write-Verbose -Message ('New-SelfContainedAssetReport: Exporting to excel...')
                # First make sure we have data to export, this shlould also weed out non-data sections meant for html
                #  (like section breaks and such)
                $ProcessExcelReport = $false
                foreach ($ReportSection in $SortedReports)
                {
                    if ($ReportContainer['Sections'][$ReportSection.Section]['AllData'].Count -gt 0)
                    {
                        $ProcessExcelReport = $true
                    }
                }

                #region Excel
                if ($ProcessExcelReport)
                {
                    # Create the excel workbook
                    try
                    {
                        $Excel = New-Object -ComObject Excel.Application -ErrorAction Stop
                        $ExcelExists = $True
                        $Excel.visible = $True
                        #Start-Sleep -s 1
                        $Workbook = $Excel.Workbooks.Add()
                        $Excel.DisplayAlerts = $false
                    }
                    catch
                    {
                        Write-Warning ('Issues opening excel: {0}' -f $_.Exception.Message)
                        $ExcelExists = $False
                    }
                    if ($ExcelExists)
                    {
                        # going through every section, but in reverse so it shows up in the correct
                        #  sheet in excel. 
                        $SortedExcelReports = $SortedReports | Sort-Object Order -Descending
                        Foreach ($ReportSection in $SortedExcelReports)
                        {
                            $SectionData = $ReportContainer['Sections'][$ReportSection.Section]['AllData']
                            $SectionProperties = $ReportContainer['Sections'][$ReportSection.Section]['ReportTypes'][$ReportType]['Properties']
                            
                            # Gather all the asset information in the section (remember that each asset may
                            #  be pointing to an array of psobjects)
                            $TransformedSectionData = @()                        
                            foreach ($asset in $SectionData.Keys)
                            {
                                # Get all of our calculated properties, then add in the asset name
                                $TempProperties = $SectionData[$asset] | Select $SectionProperties
                                $TransformedSectionData += ($TempProperties | Select @{n='AssetName';e={$asset}},*)
                            }
                            if (($TransformedSectionData.Count -gt 0) -and ($TransformedSectionData -ne $null))
                            {
                                $temparray1 = $TransformedSectionData | ConvertTo-MultiArray
                                if ($temparray1 -ne $null)
                                {    
                                    $temparray = $temparray1.Value
                                    $starta = [int][char]'a' - 1
                                    
                                    if ($temparray.GetLength(1) -gt 26) 
                                    {
                                        $col = [char]([int][math]::Floor($temparray.GetLength(1)/26) + $starta) + [char](($temparray.GetLength(1)%26) + $Starta)
                                    } 
                                    else 
                                    {
                                        $col = [char]($temparray.GetLength(1) + $starta)
                                    }
                                    
                                    Start-Sleep -s 1
                                    $xlCellValue = 1
                                    $xlEqual = 3
                                    $BadColor = 13551615    #Light Red
                                    $BadText = -16383844    #Dark Red
                                    $GoodColor = 13561798    #Light Green
                                    $GoodText = -16752384    #Dark Green
                                    $Worksheet = $Workbook.Sheets.Add()
                                    $Worksheet.Name = $ReportSection.Section
                                    $Range = $Worksheet.Range("a1","$col$($temparray.GetLength(0))")
                                    $Range.Value2 = $temparray

                                    #Format the end result (headers, autofit, et cetera)
                                    [void]$Range.EntireColumn.AutoFit()
                                    [void]$Range.FormatConditions.Add($xlCellValue,$xlEqual,'TRUE')
                                    $Range.FormatConditions.Item(1).Interior.Color = $GoodColor
                                    $Range.FormatConditions.Item(1).Font.Color = $GoodText
                                    [void]$Range.FormatConditions.Add($xlCellValue,$xlEqual,'OK')
                                    $Range.FormatConditions.Item(2).Interior.Color = $GoodColor
                                    $Range.FormatConditions.Item(2).Font.Color = $GoodText
                                    [void]$Range.FormatConditions.Add($xlCellValue,$xlEqual,'FALSE')
                                    $Range.FormatConditions.Item(3).Interior.Color = $BadColor
                                    $Range.FormatConditions.Item(3).Font.Color = $BadText
                                    
                                    # Header
                                    $range = $Workbook.ActiveSheet.Range("a1","$($col)1")
                                    $range.Interior.ColorIndex = 19
                                    $range.Font.ColorIndex = 11
                                    $range.Font.Bold = $True
                                    $range.HorizontalAlignment = -4108
                                }
                            }
                        }
                        # Get rid of the blank default worksheets
                        $Workbook.Worksheets.Item("Sheet1").Delete()
                        $Workbook.Worksheets.Item("Sheet2").Delete()
                        $Workbook.Worksheets.Item("Sheet3").Delete()
                    }
                }
                #endregion Excel
            }

            foreach ($Asset in $AssetNames)
            {
                # First check if there is any data to report upon for each asset
                $ContainsData = $false
                $SectionCount = 0
                Foreach ($ReportSection in $SortedReports)
                {
                    if ($ReportContainer['Sections'][$ReportSection.Section]['AllData'].ContainsKey($Asset))
                    {
                        $ContainsData = $true
                    }
                }
                
                # If we have any data then we have a report to create
                if ($ContainsData)
                {
                    $AssetReport = ''
                    $AssetReport += $HTMLRendering['ServerBegin'][$HTMLMode] -replace '<0>',$Asset
                    $UsedSections = 0
                    $TotalSectionsPerRow = 0
                    
                    Foreach ($ReportSection in $SortedReports)
                    {
                        if ($ReportContainer['Sections'][$ReportSection.Section]['ReportTypes'][$ReportType])
                        {
                            #region Section Calculation
                            # Use this code to track where we are at in section usage
                            #  and create new section groups as needed
                            
                            # Current section type
                            $CurrContainer = $ReportContainer['Sections'][$ReportSection.Section]['ReportTypes'][$ReportType]['ContainerType']
                            
                            # Grab first two digits found in the section container div
                            $SectionTracking = ([Regex]'\d{1}').Matches($HTMLRendering['SectionContainers'][$HTMLMode][$CurrContainer]['Head'])
                            if (($SectionTracking[1].Value -ne $TotalSectionsPerRow) -or `
                                ($SectionTracking[0].Value -eq $SectionTracking[1].Value) -or `
                                (($UsedSections + [int]$SectionTracking[0].Value) -gt $TotalSectionsPerRow) -and `
                                (!$ReportContainer['Sections'][$ReportSection.Section]['ReportTypes'][$ReportType]['SectionOverride']))
                            {
                                $NewGroup = $true
                            }
                            else
                            {
                                $NewGroup = $false
                                $UsedSections += [int]$SectionTracking[0].Value
                            }
                            
                            if ($NewGroup)
                            {
                                if ($UsedSections -ne 0)
                                {
                                    $AssetReport += $HTMLRendering['SectionContainerGroup'][$HTMLMode]['Tail']
                                }
                                $AssetReport += $HTMLRendering['SectionContainerGroup'][$HTMLMode]['Head']
                                $UsedSections = [int]$SectionTracking[0].Value
                                $TotalSectionsPerRow = [int]$SectionTracking[1].Value
                            }
                            #endregion Section Calculation
                            $AssetReport += Create-ReportSection  -Rpt $ReportContainer `
                                                                  -Asset $Asset `
                                                                  -Section $ReportSection.Section `
                                                                  -TableTitle $ReportContainer['Sections'][$ReportSection.Section]['Title']
                        }
                    }
                    
                    $AssetReport += $HTMLRendering['SectionContainerGroup'][$HTMLMode]['Tail']
                    $AssetReport += $HTMLRendering['ServerEnd'][$HTMLMode]
                    $AssetReports += $AssetReport
                    
                }
                # If we are creating per-asset reports then create one now, otherwise keep going
                if (($OutputMethod -eq 'IndividualReport') -and ($AssetReports -ne ''))
                {
                    $ReportOutputSplat.Report = ($HTMLRendering['Header'][$HTMLMode] -replace '<0>',$Asset) + 
                                                $AssetReports + 
                                                $HTMLRendering['Footer'][$HTMLMode]
                    $ReportOutputSplat.ReportName = $ReportNamePrefix + $Asset + '.html'
                    $ReportOutputSplat.ReportPath = $ReportLocation
            
                    $FinishedReportPath = New-ReportOutput @ReportOutputSplat
                    if ($FinishedReportPath -ne $false)
                    {
                        $FinishedReportPaths += $FinishedReportPath
                    }
                    $AssetReports = ''
                }
            }
            
            # If one big report is getting sent/saved do so now
            if (($OutputMethod -eq 'OneBigReport') -and ($AssetReports -ne ''))
            {
                $FullReport = ($HTMLRendering['Header'][$HTMLMode] -replace '<0>',$Asset) + 
                               $AssetReports + 
                               $HTMLRendering['Footer'][$HTMLMode]
                $ReportOutputSplat.ReportName = $ReportName
                $ReportOutputSplat.ReportPath = $ReportLocation
                $ReportOutputSplat.Report = ($HTMLRendering['Header'][$HTMLMode] -replace '<0>','Multiple Systems') + 
                                                    $AssetReports + 
                                                    $HTMLRendering['Footer'][$HTMLMode]
                $FinishedReportPath = New-ReportOutput @ReportOutputSplat
                if ($FinishedReportPath -ne $false)
                {
                    $FinishedReportPaths += $FinishedReportPath
                }
            }
            
            if ($ZipReport)
            {
                $ZipReportName = "$($ReportOutputSplat.ReportName).zip"
                $FinishedReportPaths | Add-Zip $ZipReportName
                $FinishedReportPaths | Remove-Item
                $FinishedReportPaths = @($ZipReportName)
            }
            if ($SendMail)
            {
                $ReportDeliverySplat = @{
                    'EmailSender' = $EmailSender
                    'EmailRecipient' = $EmailRecipient
                    'EmailSubject' = $EmailSubject
                    'EmailRelay' = $EmailRelay
                    'SendMail' = $SendMail
                    'ForceAnonymous' = $ForceAnonymous
                }
                
                if ($ZipReport -or ($FinishedReportPaths.Count -gt 1))
                {}
                New-ReportDelivery @ReportDeliverySplat
            }
        }
    }
}

Function Load-AssetDataFile ($FileToLoad)
{
    $ReportStructure = Import-Clixml -Path $FileToLoad
    # Export/Import XMLCLI isn't going to deal with our embedded scriptblocks (named expressions)
    # so we manually convert them back to scriptblocks like the rockstars we are...
    Foreach ($Key in $ReportStructure['Sections'].Keys) 
    {
        if ($ReportStructure['Sections'][$Key]['Type'] -eq 'Section')  # if not a section break
        {
            Foreach ($ReportTypeKey in $ReportStructure['Sections'][$Key]['ReportTypes'].Keys)
            {
                $ReportStructure['Sections'][$Key]['ReportTypes'][$ReportTypeKey]['Properties'] | 
                    ForEach {
                        $_['e'] = [Scriptblock]::Create($_['e'])
                    }
            }
        }
    }
    Return $ReportStructure
}
#endregion Functions - Asset Report Project

#region Main
$reportsplat = @{}
if ($LoadData)
{
    if (Test-Path ("forest_" + $DataFile))
    {
        $ADForestReport = Load-AssetDataFile "forest_$DataFile"
    }
    if (Test-Path ("domain_" + $DataFile))
    {
        $ADDomainReport = Load-AssetDataFile "domain_$DataFile"
    }
    $reportsplat.SkipInformationGathering = $true
}
elseif ($SaveData)
{
    $reportsplat.SaveData = $true
    $reportsplat.SaveDataFile = $DataFile
}

if ($Verbosity)
{
    $reportsplat.Verbose = $true
}

switch ($ReportFormat) {
	'HTML' {
        $reportsplat.SaveReport = $true
        $reportsplat.OutputMethod = 'IndividualReport'
	}
	'Excel' {
        $reportsplat.NoReport = $true
        $reportsplat.ReportType = 'ExportToExcel'
        $reportsplat.ExportToExcel = $true
	}
    'Custom' {
        # Fill this out as you see fit
	}
}

switch ($ReportType)
{
    { @("Forest", "ForestAndDomain") -contains $_ } {
        # Create a new forest report
        New-SelfContainedAssetReport `
                -ReportContainer $ADForestReport `
                -ReportNamePrefix 'forest_' `
                @reportsplat
    }
    { @("Domain", "ForestAndDomain") -contains $_ } {
        # Create a new per-domain report
        New-SelfContainedAssetReport `
                -ReportContainer $ADDomainReport `
                -ReportNamePrefix 'domain_' `
                @reportsplat
    }
    'Custom' {
        # Fill out as you wish
    }
}
#endregion Main
Mark Rochesterhttps://exchange2019.com
Mark currently works in the cloud space assisting large companies to migrate from either on premises to the cloud, or cloud to cloud. His experience with Enterprise migrations spans more than 25 years which basically makes him old. However, with all the oldness creeping up he still finds technology massively exciting. Please reach out for a chat anytime you would like. 🙂

Most Popular

Recent Comments