diff --git a/AsBuiltReport.Microsoft.AD.Style.ps1 b/AsBuiltReport.Microsoft.AD.Style.ps1 index 354ce3e..932c374 100644 --- a/AsBuiltReport.Microsoft.AD.Style.ps1 +++ b/AsBuiltReport.Microsoft.AD.Style.ps1 @@ -11,6 +11,7 @@ Style -Name 'Heading 1' -Size 16 -Color '0078D4' Style -Name 'Heading 2' -Size 14 -Color '00447C' Style -Name 'Heading 3' -Size 13 -Color '0081FF' Style -Name 'Heading 4' -Size 12 -Color '0077B7' +Style -Name 'NO TOC Heading 4' -Size 12 -Color '0077B7' Style -Name 'Heading 5' -Size 11 -Color '1A9BA3' Style -Name 'NO TOC Heading 5' -Size 11 -Color '1A9BA3' Style -Name 'Heading 6' -Size 10 -Color '505050' diff --git a/AsBuiltReport.Microsoft.AD.json b/AsBuiltReport.Microsoft.AD.json index 75b146d..5b84cf8 100644 --- a/AsBuiltReport.Microsoft.AD.json +++ b/AsBuiltReport.Microsoft.AD.json @@ -11,9 +11,13 @@ "Options": { "ShowDefinitionInfo": false, "PSDefaultAuthentication": "Negotiate", + "EnableCharts": false, "Exclude": { "Domains": [], "DCs": [] + }, + "Include": { + "Domains": [] } }, "InfoLevel": { @@ -31,12 +35,14 @@ "DFS": true, "SPN": true, "DuplicateObject": true, - "Security": true + "Security": true, + "BestPractice": true }, "DomainController": { "Diagnostic": true, "Services": true, - "Software": true + "Software": true, + "BestPractice": true }, "Site": { "Replication": true, @@ -45,11 +51,14 @@ "DNS": { "Aging": true, "DP": true, - "Zones": true + "Zones": true, + "BestPractice": true + }, "CA": { "Status": true, - "Statistics": true + "Statistics": true, + "BestPractice": true } } } diff --git a/AsBuiltReport.Microsoft.AD.psd1 b/AsBuiltReport.Microsoft.AD.psd1 index 2a94c71..b35a497 100644 --- a/AsBuiltReport.Microsoft.AD.psd1 +++ b/AsBuiltReport.Microsoft.AD.psd1 @@ -12,7 +12,7 @@ RootModule = 'AsBuiltReport.Microsoft.AD.psm1' # Version number of this module. -ModuleVersion = '0.7.12' +ModuleVersion = '0.7.13' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ef884..c4b6ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # :arrows_clockwise: Microsoft AD As Built Report Changelog +## [0.7.13] - 2023-06-22 + +### Added + +- Added Option "Include.Domains" to allow only a list of Active Directory Domain to document + - Include Domains in AD services + - Include Domains in DNS services +- Added Site Connection Objects section + +### Changed + +- Major improvements to health check recommendations + +### Fixed + +- Fix HealthCheck sections not working after v0.7.12 +- Fix [#84](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/84) +- Fix [#98](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/98) +- Fix [#99](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/99) +- Fix [#100](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/100) +- Fix [#101](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/101) +- Fix [#102](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/102) +- Fix [#103](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/103) +- Fix [#104](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/104) +- Fix [#105](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/105) +- Fix [#106](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/106) +- Fix [#107](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/107) +- Fix [#108](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/108) +- Fix [#109](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/109) +- Fix [#110](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/110) +- Fix Node.js 12 actions are deprecated warning message +- Fix the code to ensure that PSRemoting sessions are removed when they are no longer needed. + ## [0.7.12] - 2023-05-23 ### Changed diff --git a/README.md b/README.md index 5b21ecd..2d6fb91 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,11 @@ The **Options** schema allows certain options within the report to be toggled on | Sub-Schema | Setting | Default | Description | |-----------------|--------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ShowDefinitionInfo | true/false | false | Toggle to enable/disable Microsoft AD term explanations -| PSDefaultAuthentication | Negotiate/Kerberos | Negotiate | Allow to set the value of the PSRemoting authentication method. For Workgroup authentication Negotiate value is required. -| Exclude.DCs | Array List | Empty | Allow to filter on AD Domain Controller Server FQDN. -| Exclude.Domains | Array List | Empty | Allow to filter on AD Domain FQDN +| PSDefaultAuthentication | Negotiate/Kerberos | Negotiate | Allow to set the value of the PSRemoting authentication method. For Workgroup authentication Negotiate value is required. | +| Exclude.Domains | Array List | Empty | Allow to filter on AD Domain FQDN | +| Exclude.DCs | Array List | Empty | Allow to filter on AD Domain Controller Server FQDN. | +| Include.Domains | Array List | Empty | Allow only a list of Active Directory Domain Controller FQDN to document. | +| Include.DCs | Array List | Empty | Allow only a list of Active Directory Domain FQDN to document. | ### InfoLevel diff --git a/Src/Private/Get-AbrADCAAIA.ps1 b/Src/Private/Get-AbrADCAAIA.ps1 index 7e1aabd..2f89ddc 100644 --- a/Src/Private/Get-AbrADCAAIA.ps1 +++ b/Src/Private/Get-AbrADCAAIA.ps1 @@ -5,7 +5,7 @@ function Get-AbrADCAAIA { .DESCRIPTION .NOTES - Version: 0.7.9 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -58,12 +58,12 @@ function Get-AbrADCAAIA { $OutObj | Table @TableParams } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Authority Information Access Item)" + Write-PscriboMessage -IsWarning "Authority Information Access Item $($URI.RegURI) Section: $($_.Exception.Message)" } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Authority Information Access Table)" + Write-PscriboMessage -IsWarning "Authority Information Access Section: $($_.Exception.Message)" } } } diff --git a/Src/Private/Get-AbrADCACRLSetting.ps1 b/Src/Private/Get-AbrADCACRLSetting.ps1 index 900601d..9a8f95c 100644 --- a/Src/Private/Get-AbrADCACRLSetting.ps1 +++ b/Src/Private/Get-AbrADCACRLSetting.ps1 @@ -5,7 +5,7 @@ function Get-AbrADCACRLSetting { .DESCRIPTION .NOTES - Version: 0.7.9 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -48,12 +48,12 @@ function Get-AbrADCACRLSetting { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "CRL Validity Period $($VP.Name) Section: $($_.Exception.Message)" } } } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "CRL Validity Period Section: $($_.Exception.Message)" } $TableParams = @{ @@ -82,12 +82,12 @@ function Get-AbrADCACRLSetting { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "CRL Validity Period $($Flag.Name) Section: $($_.Exception.Message)" } } } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "CRL Validity Period Table Section: $($_.Exception.Message)" } $TableParams = @{ @@ -102,7 +102,7 @@ function Get-AbrADCACRLSetting { } } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "CRL Validity Period Section: $($_.Exception.Message)" } try { Section -Style Heading5 "CRL Distribution Point" { diff --git a/Src/Private/Get-AbrADDCDiag.ps1 b/Src/Private/Get-AbrADDCDiag.ps1 index a7a3fc6..c3701cd 100644 --- a/Src/Private/Get-AbrADDCDiag.ps1 +++ b/Src/Private/Get-AbrADDCDiag.ps1 @@ -74,7 +74,7 @@ function Get-AbrADDCDiag { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "Active Directory DCDiag $($Result.TestName) Section: $($_.Exception.Message)" } } if ($HealthCheck.DomainController.Diagnostic) { @@ -93,7 +93,7 @@ function Get-AbrADDCDiag { } } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "Active Directory DCDiag Section: $($_.Exception.Message)" } } } diff --git a/Src/Private/Get-AbrADDCRoleFeature.ps1 b/Src/Private/Get-AbrADDCRoleFeature.ps1 index 8747463..002736c 100644 --- a/Src/Private/Get-AbrADDCRoleFeature.ps1 +++ b/Src/Private/Get-AbrADDCRoleFeature.ps1 @@ -5,7 +5,7 @@ function Get-AbrADDCRoleFeature { .DESCRIPTION .NOTES - Version: 0.7.6 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -43,15 +43,21 @@ function Get-AbrADDCRoleFeature { $inObj = [ordered] @{ 'Name' = $Feature.DisplayName 'Parent' = $Feature.FeatureType - 'InstallState' = $Feature.Description + 'Description' = $Feature.Description } $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Roles Item)" + Write-PscriboMessage -IsWarning "Roles $($Feature.DisplayName) Section: $($_.Exception.Message)" } } + if ($HealthCheck.DomainController.BestPractice) { + + $OutObj | Where-Object {$_.'Name' -notin @('Active Directory Domain Services','DNS Server','File and Storage Services','DHCP Server')} | Set-Style -Style Warning + + } + $TableParams = @{ Name = "Roles - $($DC.ToString().split('.')[0].ToUpper())" List = $false @@ -61,15 +67,16 @@ function Get-AbrADDCRoleFeature { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Table @TableParams - if ($HealthCheck.DomainController.Software) { + if ($HealthCheck.DomainController.Software -and ($OutObj | Where-Object {$_.'Name' -notin @('Active Directory Domain Services','DNS Server','File and Storage Services')})) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Best Practices: Domain Controllers should have limited software and agents installed including roles and services. Non-essential code running on Domain Controllers is a risk to the enterprise Active Directory environment. A Domain Controller should only run required software, services and roles critical to essential operation." -Italic -Bold } } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Role Section)" + Write-PscriboMessage -IsWarning "Roles Section: $($_.Exception.Message)" } } diff --git a/Src/Private/Get-AbrADDFSHealth.ps1 b/Src/Private/Get-AbrADDFSHealth.ps1 index 6493de0..4b6b827 100644 --- a/Src/Private/Get-AbrADDFSHealth.ps1 +++ b/Src/Private/Get-AbrADDFSHealth.ps1 @@ -5,7 +5,7 @@ function Get-AbrADDFSHealth { .DESCRIPTION .NOTES - Version: 0.7.9 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -28,15 +28,15 @@ function Get-AbrADDFSHealth { } process { - if ($Domain -and $HealthCheck.Domain.DFS) { + if ($HealthCheck.Domain.DFS) { try { if ($Options.Exclude.DCs) { - $DFS = Get-WinADDFSHealth -SkipAutodetection -Domain $Domain -Credential $Credential | Where-Object {$_.DomainController -notin ($Options.Exclude.DCs).split(".", 2)[0]} - } Else {$DFS = Get-WinADDFSHealth -SkipAutodetection -Domain $Domain -Credential $Credential} + $DFS = Get-WinADDFSHealth -Domain $Domain -Credential $Credential | Where-Object {$_.DomainController -notin ($Options.Exclude.DCs).split(".", 2)[0]} + } Else {$DFS = Get-WinADDFSHealth -Domain $Domain -Credential $Credential} Write-PscriboMessage "Discovered AD Domain DFS Health information from $Domain." if ($DFS) { - Section -ExcludeFromTOC -Style NOTOCHeading5 'DFS Health' { - Paragraph "The following section details Distributed File System health status for Domain $($Domain.ToString().ToUpper())." + Section -ExcludeFromTOC -Style NOTOCHeading5 'Sysvol Replication Status' { + Paragraph "The following section details the sysvol folder replication status for Domain $($Domain.ToString().ToUpper())." BlankLine $OutObj = @() foreach ($DCStatus in $DFS) { @@ -44,7 +44,7 @@ function Get-AbrADDFSHealth { Write-PscriboMessage "Collecting DFS information from $($Domain)." $inObj = [ordered] @{ 'DC Name' = $DCStatus.DomainController - 'Replication State' = $DCStatus.ReplicationState + 'Replication Status' = $DCStatus.ReplicationState 'GPO Count' = $DCStatus.GroupPolicyCount 'Sysvol Count' = $DCStatus.SysvolCount 'Identical Count' = ConvertTo-TextYN $DCStatus.IdenticalCount @@ -54,16 +54,30 @@ function Get-AbrADDFSHealth { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (DFS Health Item)" + Write-PscriboMessage -IsWarning "Sysvol Replication Status Iten Section: $($_.Exception.Message)" } } if ($HealthCheck.Domain.DFS) { + $ReplicationStatusError = @( + 'Uninitialized', + 'Auto recovery', + 'In error state', + 'Disabled', + 'Unknown' + ) + $ReplicationStatusWarn = @( + 'Initialized', + 'Initial synchronization' + ) $OutObj | Where-Object { $_.'Identical Count' -like 'No' } | Set-Style -Style Warning -Property 'Identical Count' + $OutObj | Where-Object { $_.'Replication Status' -eq 'Normal' } | Set-Style -Style OK -Property 'Replication Status' + $OutObj | Where-Object { $_.'Replication Status' -in $ReplicationStatusError } | Set-Style -Style Critical -Property 'Replication Status' + $OutObj | Where-Object { $_.'Replication Status' -in $ReplicationStatusWarn } | Set-Style -Style Warning -Property 'Replication Status' } $TableParams = @{ - Name = "Domain Last Backup - $($Domain.ToString().ToUpper())" + Name = "Sysvol Replication Status - $($Domain.ToString().ToUpper())" List = $false ColumnWidths = 20, 16, 16, 16, 16, 16 } @@ -71,14 +85,18 @@ function Get-AbrADDFSHealth { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - $OutObj | Sort-Object -Property 'Naming Context' | Table @TableParams - Paragraph "Health Check:" -Italic -Bold -Underline - Paragraph "Corrective Actions: Ensure an identical GPO/SYSVOL content for the domain controller in all Active Directory domains." -Italic -Bold + $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams + if ($HealthCheck.Domain.DFS -and (($OutObj | Where-Object { $_.'Identical Count' -like 'No' }) -or ($OutObj | Where-Object { $_.'Replication Status' -in $ReplicationStatusError }))) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Corrective Actions: SYSVOL is a special directory that resides on each domain controller (DC) within a domain. The directory comprises folders that store Group Policy objects (GPOs) and logon scripts that clients need to access and synchronize between DCs. For these logon scripts and GPOs to function properly, SYSVOL should be replicated accurately and rapidly throughout the domain. Ensure that proper SYSVOL replication is in place to ensure identical GPO/SYSVOL content for the domain controller across all Active Directory domains." -Italic -Bold + BlankLine + } } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (DFS Health Table)" + Write-PscriboMessage -IsWarning "Sysvol Replication Status Table Section: $($_.Exception.Message)" } try { Write-PscriboMessage "Discovered AD Domain Sysvol Health information from $Domain." @@ -92,7 +110,7 @@ function Get-AbrADDFSHealth { 'TotalSize'= '{0:N2}' -f ((($_.group | Measure-Object length -Sum).Sum) /1MB) } } | Sort-Object -Descending -Property 'Totalsize'} if ($SYSVOLFolder) { - Section -ExcludeFromTOC -Style NOTOCHeading5 'Sysvol Folder Status' { + Section -ExcludeFromTOC -Style NOTOCHeading5 'Sysvol Content Status' { Paragraph "The following section details domain $($Domain.ToString().ToUpper()) sysvol health status." BlankLine $OutObj = @() @@ -107,7 +125,7 @@ function Get-AbrADDFSHealth { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Sysvol Health Item)" + Write-PscriboMessage -IsWarning "Sysvol Health $($Extension.Extension) Section: $($_.Exception.Message)" } } @@ -116,7 +134,7 @@ function Get-AbrADDFSHealth { } $TableParams = @{ - Name = "Sysvol Folder Status - $($Domain.ToString().ToUpper())" + Name = "Sysvol Content Status - $($Domain.ToString().ToUpper())" List = $false ColumnWidths = 33, 33, 34 } @@ -127,13 +145,17 @@ function Get-AbrADDFSHealth { $OutObj | Sort-Object -Property 'Extension' | Table @TableParams if ($OutObj | Where-Object { $_.'Extension' -notin ('.bat','.exe','.nix','.vbs','.pol','.reg','.xml','.admx','.adml','.inf','.ini','.adm','.kix','.msi','.ps1','.cmd','.ico')}) { Paragraph "Health Check:" -Italic -Bold -Underline - Paragraph "Corrective Actions: Make sure Sysvol content has no malicious extensions or unnecessary content." -Italic -Bold + BlankLine + Paragraph "Corrective Actions: Make sure Sysvol folder has no malicious extensions or unnecessary content." -Italic -Bold } } } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Sysvol Health Table)" + Write-PscriboMessage -IsWarning "Sysvol Health Table Section: $($_.Exception.Message)" } try { Write-PscriboMessage "Discovered AD Domain Netlogon Health information from $Domain." @@ -147,7 +169,7 @@ function Get-AbrADDFSHealth { 'TotalSize'= '{0:N2}' -f ((($_.group | Measure-Object length -Sum).Sum) /1MB) } } | Sort-Object -Descending -Property 'Totalsize'} if ($NetlogonFolder) { - Section -ExcludeFromTOC -Style NOTOCHeading5 'Netlogon Folder Status' { + Section -ExcludeFromTOC -Style NOTOCHeading5 'Netlogon Content Status' { Paragraph "The following section details domain $($Domain.ToString().ToUpper()) netlogon health status." BlankLine $OutObj = @() @@ -162,7 +184,7 @@ function Get-AbrADDFSHealth { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Netlogon Health Item)" + Write-PscriboMessage -IsWarning "Netlogon Health $($Extension.Extension) Section: $($_.Exception.Message)" } } @@ -171,7 +193,7 @@ function Get-AbrADDFSHealth { } $TableParams = @{ - Name = "Netlogon Folder Status - $($Domain.ToString().ToUpper())" + Name = "Netlogon Content Status - $($Domain.ToString().ToUpper())" List = $false ColumnWidths = 33, 33, 34 } @@ -182,13 +204,17 @@ function Get-AbrADDFSHealth { $OutObj | Sort-Object -Property 'Extension' | Table @TableParams if ($OutObj | Where-Object { $_.'Extension' -notin ('.bat','.exe','.nix','.vbs','.pol','.reg','.xml','.admx','.adml','.inf','.ini','.adm','.kix','.msi','.ps1','.cmd','.ico')}) { Paragraph "Health Check:" -Italic -Bold -Underline - Paragraph "Corrective Actions: Make sure Netlogon content has no malicious extensions or unnecessary content." -Italic -Bold + BlankLine + Paragraph "Corrective Actions: Make sure Netlogon folder has no malicious extensions or unnecessary content." -Italic -Bold } } } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Sysvol Health Table)" + Write-PscriboMessage -IsWarning "Sysvol Health Section: $($_.Exception.Message)" } } } diff --git a/Src/Private/Get-AbrADDNSInfrastructure.ps1 b/Src/Private/Get-AbrADDNSInfrastructure.ps1 index 49d182e..b1ccc5c 100644 --- a/Src/Private/Get-AbrADDNSInfrastructure.ps1 +++ b/Src/Private/Get-AbrADDNSInfrastructure.ps1 @@ -5,7 +5,7 @@ function Get-AbrADDNSInfrastructure { .DESCRIPTION .NOTES - Version: 0.7.6 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -51,7 +51,7 @@ function Get-AbrADDNSInfrastructure { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning " $($_.Exception.Message) (Infrastructure Summary)" + Write-PscriboMessage -IsWarning "DNS Infrastructure Summary Section: $($_.Exception.Message)" } } } @@ -91,12 +91,12 @@ function Get-AbrADDNSInfrastructure { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "$($DC.ToString().ToUpper().Split(".")[0]) DNS IP Configuration Section: $($_.Exception.Message)" } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (DNS IP Configuration Item)" + Write-PscriboMessage -IsWarning "Domain Controller DNS IP Configuration Table Section: $($_.Exception.Message)" } } } @@ -119,12 +119,13 @@ function Get-AbrADDNSInfrastructure { $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams if ($HealthCheck.DNS.DP -and ($OutObj | Where-Object { $_.'DNS IP 1' -eq "127.0.0.1"})) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Best Practices: DNS configuration on network adapter should include the loopback address, but not as the first entry." -Italic -Bold } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (DNS IP Configuration Table)" + Write-PscriboMessage -IsWarning "Domain Controller DNS IP Configuration Section: $($_.Exception.Message)" } } #---------------------------------------------------------------------------------------------# @@ -147,7 +148,7 @@ function Get-AbrADDNSInfrastructure { $inObj = [ordered] @{ 'Name' = $Partition.DirectoryPartitionName 'State' = Switch ($Partition.State) { - $Null {'-'} + $Null {'--'} 0 {'DNS_DP_OKAY'} 1 {'DNS_DP_STATE_REPL_INCOMING'} 2 {'DNS_DP_STATE_REPL_OUTGOING'} @@ -160,12 +161,12 @@ function Get-AbrADDNSInfrastructure { $OutObj += [pscustomobject]$inobj } catch { - Write-PscriboMessage -IsWarning $_.Exception.Message + Write-PscriboMessage -IsWarning "Directory Partitions Item Section: $($_.Exception.Message)" } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Directory Partitions Item)" + Write-PscriboMessage -IsWarning "Directory Partitions Table Section: $($_.Exception.Message)" } $TableParams = @{ @@ -183,7 +184,7 @@ function Get-AbrADDNSInfrastructure { } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Directory Partitions Table)" + Write-PscriboMessage -IsWarning "Directory Partitions Section: $($_.Exception.Message)" } } #---------------------------------------------------------------------------------------------# @@ -282,6 +283,7 @@ function Get-AbrADDNSInfrastructure { $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams if ($HealthCheck.DNS.Zones -and ($OutObj | Where-Object { $_.'Scavenging State' -eq 'Disabled'})) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Best Practices: Microsoft recommends to enable aging/scavenging on all DNS servers. However, with AD-integrated zones ensure to enable DNS scavenging on one DC at main site. The results will be replicated to other DCs." -Italic -Bold } } @@ -364,7 +366,7 @@ function Get-AbrADDNSInfrastructure { $TableParams = @{ Name = "Root Hints - $($Domain.ToString().ToUpper())" List = $false - ColumnWidths = 50, 50 + ColumnWidths = 40, 60 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" diff --git a/Src/Private/Get-AbrADDNSZone.ps1 b/Src/Private/Get-AbrADDNSZone.ps1 index decc2c3..f37178c 100644 --- a/Src/Private/Get-AbrADDNSZone.ps1 +++ b/Src/Private/Get-AbrADDNSZone.ps1 @@ -158,7 +158,8 @@ function Get-AbrADDNSZone { } $OutObj | Table @TableParams if ($HealthCheck.DNS.Zones -and ($OutObj | Where-Object { $_.'Secure Secondaries' -eq "Send zone transfers to all secondary servers that request them."})) { - Paragraph "Health Check:" -Italic -Bold -Underline + Paragraph "Health Check:" -Italic -Bold + BlankLine Paragraph "Best Practices: Configure all DNS zones only to allow zone transfers from Trusted IP addresses." -Italic -Bold } } diff --git a/Src/Private/Get-AbrADDomain.ps1 b/Src/Private/Get-AbrADDomain.ps1 index 7e854e7..11b0c6c 100644 --- a/Src/Private/Get-AbrADDomain.ps1 +++ b/Src/Private/Get-AbrADDomain.ps1 @@ -5,7 +5,7 @@ function Get-AbrADDomain { .DESCRIPTION .NOTES - Version: 0.7.2 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -59,8 +59,7 @@ function Get-AbrADDomain { 'Users Container' = ConvertTo-ADCanonicalName -DN $DomainInfo.UsersContainer -Domain $Domain 'ReadOnly Replica Directory Servers' = ConvertTo-EmptyToFiller $DomainInfo.ReadOnlyReplicaDirectoryServers 'ms-DS-MachineAccountQuota' = Invoke-Command -Session $TempPssSession {(Get-ADObject -Server $using:DC -Identity (($using:DomainInfo).DistinguishedName) -Properties ms-DS-MachineAccountQuota -ErrorAction SilentlyContinue).'ms-DS-MachineAccountQuota'} - 'RID Issued' = $RIDsIssued - 'RID Available' = $RIDsRemaining + 'RID Issued/Available' = "$($RIDsIssued) / $($RIDsRemaining)" } $OutObj += [pscustomobject]$inobj @@ -76,7 +75,7 @@ function Get-AbrADDomain { } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (AD Domain Summary)" + Write-PscriboMessage -IsWarning "AD Domain Summary Section: $($_.Exception.Message)" } } } diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index 0b264de..98b191c 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -70,7 +70,7 @@ function Get-AbrADDomainController { try { Write-PscriboMessage "Collecting AD Domain Controller Hardware information for domain $Domain" Section -Style Heading5 'Hardware Inventory' { - Paragraph "The following section provides detailed Domain Controller Hardware information for domain $($Domain.ToString().ToUpper())." + Paragraph "The following section provides detailed Domain Controller hardware information for domain $($Domain.ToString().ToUpper())." BlankLine Write-PscriboMessage "Discovering Active Directory Domain Controller information in $Domain." $DCHWInfo = @() @@ -108,7 +108,11 @@ function Get-AbrADDomainController { 'Number of Processors' = ($HWCPU | Measure-Object).Count 'Number of CPU Cores' = $HWCPU[0].NumberOfCores 'Number of Logical Cores' = $HWCPU[0].NumberOfLogicalProcessors - 'Physical Memory' = ConvertTo-FileSizeString $HW.CsTotalPhysicalMemory + 'Physical Memory' = &{ + try { + ConvertTo-FileSizeString $HW.CsTotalPhysicalMemory + } catch {'0.00 GB'} + } } $DCHWInfo += [pscustomobject]$inobj } @@ -122,6 +126,11 @@ function Get-AbrADDomainController { if ($InfoLevel.Domain -ge 2) { foreach ($DCHW in $DCHWInfo) { Section -ExcludeFromTOC -Style NOTOCHeading6 $($DCHW.Name.ToString().ToUpper()) { + if ($HealthCheck.DomainController.Diagnostic) { + if ([int]([regex]::Matches($DCHW.'Physical Memory', "\d+(\.*\d+)").value) -lt 8) { + $DCHW | Set-Style -Style Warning -Property 'Physical Memory' + } + } $TableParams = @{ Name = "Hardware Inventory - $($DCHW.Name.ToString().ToUpper())" List = $true @@ -131,9 +140,21 @@ function Get-AbrADDomainController { $TableParams['Caption'] = "- $($TableParams.Name)" } $DCHW | Table @TableParams + if ($HealthCheck.DomainController.Diagnostic) { + if ([int]([regex]::Matches($DCHW.'Physical Memory', "\d+(\.*\d+)").value) -lt 8) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Best Practice: Microsoft recommend putting enough RAM 8GB+ to load the entire DIT into memory, plus accommodate the operating system and other installed applications, such as anti-virus, backup software, monitoring, and so on." -Italic -Bold + } + } } } } else { + if ($HealthCheck.DomainController.Diagnostic) { + if ([int]([regex]::Matches($DCHWInfo.'Physical Memory', "\d+(\.*\d+)").value) -lt 8) { + $DCHWInfo | Set-Style -Style Warning -Property 'Physical Memory' + } + } $TableParams = @{ Name = "Hardware Inventory - $($Domain.ToString().ToUpper())" List = $false @@ -144,6 +165,13 @@ function Get-AbrADDomainController { $TableParams['Caption'] = "- $($TableParams.Name)" } $DCHWInfo | Table @TableParams + if ($HealthCheck.DomainController.Diagnostic) { + if ([int]([regex]::Matches($DCHWInfo.'Physical Memory', "\d+(\.*\d+)").value) -lt 8) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Best Practice: Microsoft recommend putting enough RAM 8GB+ to load the entire DIT into memory, plus accommodate the operating system and other installed applications, such as anti-virus, backup software, monitoring, and so on." -Italic -Bold + } + } } } } @@ -354,8 +382,9 @@ function Get-AbrADDomainController { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams - if ( $OutObj | Where-Object { { $_.'KDC SRV' -eq 'Fail' } -or { $_.'PDC SRV' -eq 'Fail' } -or { $_.'GC SRV' -eq 'Fail' } -or { $_.'DC SRV' -eq 'Fail' }}) { + if ( $OutObj | Where-Object { $_.'KDC SRV' -eq 'Fail' -or $_.'PDC SRV' -eq 'Fail' -or $_.'GC SRV' -eq 'Fail' -or $_.'DC SRV' -eq 'Fail' }) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Best Practice: The SRV record is a Domain Name System (DNS) resource record. It's used to identify computers hosting specific services. SRV resource records are used to locate domain controllers for Active Directory." -Italic -Bold } } @@ -417,6 +446,7 @@ function Get-AbrADDomainController { $OutObj | Sort-Object -Property 'Name' | Table @TableParams if ($HealthCheck.DomainController.Software) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Best Practices: Do not run other software or services on a Domain Controller." -Italic -Bold } } @@ -480,6 +510,7 @@ function Get-AbrADDomainController { $OutObj | Sort-Object -Property 'Name' | Table @TableParams if ($HealthCheck.DomainController.Software) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Security Best Practices: It is critical to install security updates to protect your systems from malicious attacks. In the long run, it is also important to install software updates, not only to access new features, but also to be on the safe side in terms of security loop holes being discovered in outdated programs. And it is in your own best interest to install all other updates, which may potentially cause your system to become vulnerable to attack." -Italic -Bold } } diff --git a/Src/Private/Get-AbrADDomainLastBackup.ps1 b/Src/Private/Get-AbrADDomainLastBackup.ps1 index f443c44..bbf67ee 100644 --- a/Src/Private/Get-AbrADDomainLastBackup.ps1 +++ b/Src/Private/Get-AbrADDomainLastBackup.ps1 @@ -69,8 +69,11 @@ function Get-AbrADDomainLastBackup { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Naming Context' | Table @TableParams - Paragraph "Health Check:" -Italic -Bold -Underline - Paragraph "Corrective Actions: Ensure there is a recent (<180 days) Active Directory backup." -Italic -Bold + if ($OutObj | Where-Object { $_.'Last Backup in Days' -gt 180 }) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Corrective Actions: Ensure there is a recent (<180 days) Active Directory backup." -Italic -Bold + } } } } diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index e310287..0ecdeed 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -5,7 +5,7 @@ function Get-AbrADDomainObject { .DESCRIPTION .NOTES - Version: 0.7.11 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -28,37 +28,38 @@ function Get-AbrADDomainObject { } process { - if ($InfoLevel.Domain -ge 2) { - try { - Section -Style Heading4 'Domain Object Count' { - if ($Domain) { - Write-PscriboMessage "Collecting the Active Directory Object Count of domain $Domain." - try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $Computers = Invoke-Command -Session $TempPssSession {(Get-ADComputer -Server $using:DC -Filter * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName) | Measure-Object} - $Servers = Invoke-Command -Session $TempPssSession {(Get-ADComputer -Server $using:DC -Filter { OperatingSystem -like "Windows Ser*"} -Property OperatingSystem -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName) | Measure-Object} - $Users = Invoke-Command -Session $TempPssSession {(Get-ADUser -Server $using:DC -filter * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName) | Measure-Object} - $PrivilegedUsers = Invoke-Command -Session $TempPssSession {(Get-ADUser -Server $using:DC -filter {AdminCount -eq "1"} -Properties AdminCount -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName) | Measure-Object} - $Group = Invoke-Command -Session $TempPssSession {(Get-ADGroup -Server $using:DC -filter * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName) | Measure-Object} - $DomainController = Invoke-Command -Session $TempPssSession {(Get-ADDomainController -Server $using:DC -filter *) | Select-Object name | Measure-Object} - $GC = Invoke-Command -Session $TempPssSession {(Get-ADDomainController -Server $using:DC -filter {IsGlobalCatalog -eq "True"}) | Select-Object name | Measure-Object} + try { + Section -Style Heading4 'Domain Object Stats' { + if ($Domain) { + Write-PscriboMessage "Collecting the Active Directory Object Count of domain $Domain." + try { + $ADLimitedProperties = @("Name","Enabled","SAMAccountname","DisplayName","Enabled","LastLogonDate","PasswordLastSet","PasswordNeverExpires","PasswordNotRequired","PasswordExpired","SmartcardLogonRequired","AccountExpirationDate","AdminCount","Created","Modified","LastBadPasswordAttempt","badpwdcount","mail","CanonicalName","DistinguishedName","ServicePrincipalName","SIDHistory","PrimaryGroupID","UserAccountControl","CannotChangePassword","PwdLastSet","LockedOut","TrustedForDelegation","TrustedtoAuthForDelegation","msds-keyversionnumber","SID") + $script:DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $script:Computers = Invoke-Command -Session $TempPssSession {(Get-ADComputer -ResultPageSize 1000 -Server $using:DC -Filter * -Properties Enabled,OperatingSystem,lastlogontimestamp,PasswordLastSet,SIDHistory -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName)} + $Servers = $Computers | Where-Object { $_.OperatingSystem -like "Windows Ser*" } | Measure-Object + $script:Users = Invoke-Command -Session $TempPssSession {Get-ADUser -ResultPageSize 1000 -Server $using:DC -Filter * -Property $using:ADLimitedProperties -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName } + $script:PrivilegedUsers = $Users | Where-Object {$_.AdminCount -eq 1} + $Group = Invoke-Command -Session $TempPssSession {(Get-ADGroup -Server $using:DC -filter * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName) | Measure-Object} + $DomainController = Invoke-Command -Session $TempPssSession {(Get-ADDomainController -Server $using:DC -filter *) | Select-Object name | Measure-Object} + $GC = Invoke-Command -Session $TempPssSession {(Get-ADDomainController -Server $using:DC -filter {IsGlobalCatalog -eq "True"}) | Select-Object name | Measure-Object} - try { - $OutObj = @() - $inObj = [ordered] @{ - 'Computers' = $Computers.Count - 'Servers' = $Servers.Count - } - $OutObj += [pscustomobject]$inobj + try { + $OutObj = @() + $inObj = [ordered] @{ + 'Computers' = $Computers.Count + 'Servers' = $Servers.Count + } + $OutObj += [pscustomobject]$inobj - $TableParams = @{ - Name = "Computers Object - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } + $TableParams = @{ + Name = "Computers - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { try { $sampleData = $inObj.GetEnumerator() @@ -92,7 +93,7 @@ function Get-AbrADDomainObject { Chart = $exampleChart ChartArea = $exampleChartArea Name = 'ComputersObject' - Text = 'Computers Object Count' + Text = 'Computers Count' Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) } Add-ChartTitle @addChartTitleParams @@ -102,34 +103,36 @@ function Get-AbrADDomainObject { catch { Write-PscriboMessage -IsWarning $($_.Exception.Message) } - if ($OutObj) { - Section -Style Heading4 'Computers Object' { - if ($chartFileItem) { - Image -Text 'Computers Object - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem - } - $OutObj | Table @TableParams + } + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading5 'Computers' { + if ($chartFileItem) { + Image -Text 'Computers Object - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem } + $OutObj | Table @TableParams } } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } + catch { + Write-PscriboMessage -IsWarning $($_.Exception.Message) + } + try { + $OutObj = @() + $inObj = [ordered] @{ + 'Domain Controller' = $DomainController.Count + 'Global Catalog' = $GC.Count } - try { - $OutObj = @() - $inObj = [ordered] @{ - 'Domain Controller' = $DomainController.Count - 'Global Catalog' = $GC.Count - } - $OutObj += [pscustomobject]$inobj + $OutObj += [pscustomobject]$inobj - $TableParams = @{ - Name = "Domain Controller Object - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } + $TableParams = @{ + Name = "Domain Controller - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { try { $sampleData = $inObj.GetEnumerator() @@ -163,7 +166,7 @@ function Get-AbrADDomainObject { Chart = $exampleChart ChartArea = $exampleChartArea Name = 'DomainControllerObject' - Text = 'Domain Controller Object Count' + Text = 'Domain Controller Count' Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) } Add-ChartTitle @addChartTitleParams @@ -173,35 +176,37 @@ function Get-AbrADDomainObject { catch { Write-PscriboMessage -IsWarning $($_.Exception.Message) } - if ($OutObj) { - Section -Style Heading4 'Domain Controller Object' { - if ($chartFileItem) { - Image -Text 'Domain Controller Object - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem - } - $OutObj | Table @TableParams + } + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading5 'Domain Controller' { + if ($chartFileItem) { + Image -Text 'Domain Controller Object - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem } + $OutObj | Table @TableParams } } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } + catch { + Write-PscriboMessage -IsWarning $($_.Exception.Message) + } + try { + $OutObj = @() + $inObj = [ordered] @{ + 'Users' = ($Users | Measure-Object).Count + 'Privileged Users' = ($PrivilegedUsers | Measure-Object).Count + 'Groups' = $Group.Count } - try { - $OutObj = @() - $inObj = [ordered] @{ - 'Users' = $Users.Count - 'Privileged Users' = $PrivilegedUsers.Count - 'Groups' = $Group.Count - } - $OutObj += [pscustomobject]$inobj + $OutObj += [pscustomobject]$inobj - $TableParams = @{ - Name = "User Object - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } + $TableParams = @{ + Name = "User - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { try { $sampleData = $inObj.GetEnumerator() @@ -235,7 +240,7 @@ function Get-AbrADDomainObject { Chart = $exampleChart ChartArea = $exampleChartArea Name = 'UsersObject' - Text = 'Users Object Count' + Text = 'Users Count' Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) } Add-ChartTitle @addChartTitleParams @@ -245,138 +250,54 @@ function Get-AbrADDomainObject { catch { Write-PscriboMessage -IsWarning $($_.Exception.Message) } - if ($OutObj) { - Section -Style Heading4 'Users Object' { - if ($chartFileItem) { - Image -Text 'Users Object - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem - } - $OutObj | Table @TableParams + } + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading5 'Users' { + if ($chartFileItem) { + Image -Text 'Users Object - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem } + $OutObj | Table @TableParams } } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) - } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Count)" + Write-PscriboMessage -IsWarning $($_.Exception.Message) } } - } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Count)" - } - } - try { - $OutObj = @() - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $Users = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter * -Properties *} - if ($Users) { - $Categories = @('Enabled','Disabled') - Write-PscriboMessage "Collecting User Accounts in Domain." - foreach ($Category in $Categories) { - if ($Category -eq 'Enabled') { - $Values = $Users.Enabled -eq $True - } - else {$Values = $Users.Enabled -eq $False} - $inObj = [ordered] @{ - 'Status' = $Category - 'Count' = $Values.Count - 'Percentage' = "$([math]::Round((($Values).Count / $Users.Count * 100), 0))%" - } - $OutObj += [pscustomobject]$inobj - } - - $TableParams = @{ - Name = "User Accounts in Domain - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 50, 25, 25 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - try { - $sampleData = $OutObj - - $exampleChart = New-Chart -Name UserAccountsinAD -Width 600 -Height 400 - - $addChartAreaParams = @{ - Chart = $exampleChart - Name = 'exampleChartArea' - } - $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru - - $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'Status' - YField = 'Count' - Palette = 'Blue' - ColorPerDataPoint = $true - } - $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru - - $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Status' - TitleAlignment = 'Center' - } - Add-ChartLegend @addChartLegendParams - - $addChartTitleParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'UserAccountsinAD' - Text = 'User Accounts' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) - } - Add-ChartTitle @addChartTitleParams - - $chartFileItem = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) - } - - } - if ($OutObj) { - Section -Style Heading4 'User Accounts in Domain' { - if ($chartFileItem) { - Image -Text 'User Accounts in Domain - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" } - $OutObj | Table @TableParams } } } catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" } try { $OutObj = @() $DaysInactive = 90 $dormanttime = ((Get-Date).AddDays(-90)).Date $passwordtime = (Get-Date).Adddays(-42) - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $Users = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter * -Properties *} - $CannotChangePassword = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter * -Properties * | Where-Object {$_.CannotChangePassword}} - $PasswordNextLogon = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -LDAPFilter "(pwdLastSet=0)"} - $passwordNeverExpires = Invoke-Command -Session $TempPssSession {get-aduser -Server $using:DC -filter * -properties Name, PasswordNeverExpires | Where-Object {$_.passwordNeverExpires -eq "true" }} - $SmartcardLogonRequired = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter * -Properties 'SmartcardLogonRequired' | Where-Object {$_.SmartcardLogonRequired -eq $True}} + $CannotChangePassword = $Users | Where-Object {$_.CannotChangePassword} + $PasswordNextLogon = $Users | Where-Object {$_.PasswordLastSet -eq 0 -or $_.PwdLastSet -eq 0} + $passwordNeverExpires = $Users | Where-Object { $_.passwordNeverExpires -eq "true" } + $SmartcardLogonRequired = $Users | Where-Object {$_.SmartcardLogonRequired -eq $True} $SidHistory = $Users | Select-Object -ExpandProperty SIDHistory - $PasswordLastSet = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter {PasswordNeverExpires -eq $false -and PasswordNotRequired -eq $false} -Properties PasswordLastSet,PasswordNeverExpires,PasswordNotRequired} - $NeverloggedIn = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter {(lastlogontimestamp -notlike "*")}} + $PasswordLastSet = $Users | Where-Object { $_.PasswordNeverExpires -eq $false -and $_.PasswordNotRequired -eq $false} + $NeverloggedIn = $Users | Where-Object {-not $_.LastLogonDate} $Dormant = $Users | Where-Object {($_.LastLogonDate) -lt $dormanttime} - $PasswordNotRequired = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:DC -Filter {PasswordNotRequired -eq $true}} + $PasswordNotRequired = $Users | Where-Object {$_.PasswordNotRequired -eq $true} $AccountExpired = Invoke-Command -Session $TempPssSession {Search-ADAccount -Server $using:DC -AccountExpired} $AccountLockout = Invoke-Command -Session $TempPssSession {Search-ADAccount -Server $using:DC -LockedOut} - $Categories = @('Cannot Change Password','Password Never Expires','Must Change Password at Logon','Password Age (> 42 days)','SmartcardLogonRequired','SidHistory', 'Never Logged in','Dormant (> 90 days)','Password Not Required','Account Expired','Account Lockout') + $Categories = @('Total Users','Cannot Change Password','Password Never Expires','Must Change Password at Logon','Password Age (> 42 days)','SmartcardLogonRequired','SidHistory', 'Never Logged in','Dormant (> 90 days)','Password Not Required','Account Expired','Account Lockout') if ($Categories) { Write-PscriboMessage "Collecting User Accounts in Domain." foreach ($Category in $Categories) { try { - if ($Category -eq 'Cannot Change Password') { + if ($Category -eq 'Total Users') { + $Values = $Users + } + elseif ($Category -eq 'Cannot Change Password') { $Values = $CannotChangePassword } elseif ($Category -eq 'Must Change Password at Logon') { @@ -411,12 +332,24 @@ function Get-AbrADDomainObject { } $inObj = [ordered] @{ 'Category' = $Category - 'Enabled Count' = ($Values.Enabled).Count - 'Enabled %' = [math]::Round((($Values.Enabled).Count / $Users.Count * 100), 0) - 'Disabled Count' = ($Null -eq $Values.Enabled).Count - 'Disabled %' = [math]::Round((($Null -eq $Values.Enabled).Count / $Users.Count * 100), 0) - 'Total Count' = ($Values.Enabled).Count - 'Total %' = [math]::Round((($Values.Enabled).Count / $Users.Count * 100), 0) + 'Enabled' = ($Values.Enabled -eq $True | Measure-Object).Count + 'Enabled %' = Switch ($Users.Count) { + 0 {'0'} + $Null {'0'} + default {[math]::Round((($Values.Enabled -eq $True | Measure-Object).Count / $Users.Count * 100), 2)} + } + 'Disabled' = ($Values.Enabled -eq $False | Measure-Object).Count + 'Disabled %' = Switch ($Users.Count) { + 0 {'0'} + $Null {'0'} + default {[math]::Round((($Values.Enabled -eq $False | Measure-Object).Count / $Users.Count * 100), 2)} + } + 'Total' = ($Values | Measure-Object).Count + 'Total %' = Switch ($Users.Count) { + 0 {'0'} + $Null {'0'} + default {[math]::Round((($Values | Measure-Object).Count / $Users.Count * 100), 2)} + } } $OutObj += [pscustomobject]$inobj @@ -434,48 +367,50 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - try { - $sampleData = $OutObj + if ($Options.EnableCharts) { + try { + $sampleData = $OutObj - $exampleChart = New-Chart -Name UserAccountsinAD -Width 600 -Height 400 + $exampleChart = New-Chart -Name UserAccountsinAD -Width 600 -Height 400 - $addChartAreaParams = @{ - Chart = $exampleChart - Name = 'exampleChartArea' - } - $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru + $addChartAreaParams = @{ + Chart = $exampleChart + Name = 'exampleChartArea' + } + $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru - $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'Category' - YField = 'Total Count' - Palette = 'Blue' - ColorPerDataPoint = $true - } - $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru + $addChartSeriesParams = @{ + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'exampleChartSeries' + XField = 'Category' + YField = 'Total' + Palette = 'Blue' + ColorPerDataPoint = $true + } + $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru - $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Category' - TitleAlignment = 'Center' - } - Add-ChartLegend @addChartLegendParams + $addChartLegendParams = @{ + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' + } + Add-ChartLegend @addChartLegendParams - $addChartTitleParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'StatusofUsersAccounts' - Text = 'Status of Users Accounts' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) - } - Add-ChartTitle @addChartTitleParams + $addChartTitleParams = @{ + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'StatusofUsersAccounts' + Text = 'Status of Users Accounts' + Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + } + Add-ChartTitle @addChartTitleParams - $chartFileItem = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + $chartFileItem = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru + } + catch { + Write-PscriboMessage -IsWarning $($_.Exception.Message) + } } } if ($OutObj) { @@ -491,19 +426,16 @@ function Get-AbrADDomainObject { Write-PscriboMessage -IsWarning $($_.Exception.Message) } try { - Section -Style Heading4 'Privileged Group Count' { + Section -Style Heading4 'Privileged Group Stats' { $OutObj = @() if ($Domain) { Write-PscriboMessage "Collecting Privileged Group in Active Directory." try { $DomainSID = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).domainsid.Value} - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} if ($Domain -eq $ADSystem.Name) { - #$Groups = 'Domain Admins','Enterprise Admins','Administrators','Server Operators','DnsAdmins','Remote Desktop Users','Incoming Forest Trust Builders','Key Admins','Backup Operators','Cert Publishers','Print Operators','Account Operators','Schema Admins' $GroupsSID = "$DomainSID-512","$DomainSID-519",'S-1-5-32-544','S-1-5-32-549',"$DomainSID-1101",'S-1-5-32-555','S-1-5-32-557',"$DomainSID-526",'S-1-5-32-551',"$DomainSID-517",'S-1-5-32-550','S-1-5-32-548',"$DomainSID-518" } else { - #$Groups = 'Domain Admins','Server Operators','DnsAdmins','Remote Desktop Users','Key Admins','Backup Operators','Cert Publishers','Print Operators','Account Operators' $GroupsSID = "$DomainSID-512",'S-1-5-32-549',"$DomainSID-1101",'S-1-5-32-555','S-1-5-32-557',"$DomainSID-526",'S-1-5-32-551',"$DomainSID-517",'S-1-5-32-550','S-1-5-32-548' } if ($GroupsSID) { @@ -530,7 +462,7 @@ function Get-AbrADDomainObject { } $TableParams = @{ - Name = "Privileged Group Count - $($Domain.ToString().ToUpper())" + Name = "Privileged Group Stats - $($Domain.ToString().ToUpper())" List = $false ColumnWidths = 60, 40 } @@ -540,6 +472,7 @@ function Get-AbrADDomainObject { $OutObj | Sort-Object -Property 'Group Name' | Table @TableParams if ($HealthCheck.Domain.Security -and ($OutObj | Where-Object { $_.'Group Name' -eq 'Schema Admins' -and $_.Count -gt 1 })) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Security Best Practice: The Schema Admins group is a privileged group in a forest root domain. Members of the Schema Admins group can make changes to the schema, which is the framework for the Active Directory forest. Changes to the schema are not frequently required. This group only contains the Built-in Administrator account by default. Additional accounts must only be added when changes to the schema are necessary and then must be removed." -Italic -Bold } } @@ -553,115 +486,23 @@ function Get-AbrADDomainObject { catch { Write-PscriboMessage -IsWarning $($_.Exception.Message) } - try { - $OutObj = @() - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $Computers = Invoke-Command -Session $TempPssSession {Get-ADComputer -Server $using:DC -Filter * -Properties *} - if ($Computers) { - $Categories = @('Enabled','Disabled') - Write-PscriboMessage "Collecting Computer Accounts in Domain." - foreach ($Category in $Categories) { - try { - if ($Category -eq 'Enabled') { - $Values = $Computers.Enabled -eq $True - } - else {$Values = $Computers.Enabled -eq $False} - $inObj = [ordered] @{ - 'Status' = $Category - 'Count' = $Values.Count - 'Percentage' = Switch ($Computers.Count) { - 0 {'0'} - $Null {'0'} - default {"$([math]::Round((($Values).Count / $Computers.Count * 100), 0))%"} - } - } - $OutObj += [pscustomobject]$inobj - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Computer Accounts in Domain)" - } - } - - $TableParams = @{ - Name = "Computer Accounts in Domain - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 50, 25, 25 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - try { - $sampleData = $OutObj - - $exampleChart = New-Chart -Name ComputerAccountsinAD -Width 600 -Height 400 - - $addChartAreaParams = @{ - Chart = $exampleChart - Name = 'exampleChartArea' - } - $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru - - $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'Status' - YField = 'Count' - Palette = 'Blue' - ColorPerDataPoint = $true - } - $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru - - $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Status' - TitleAlignment = 'Center' - } - Add-ChartLegend @addChartLegendParams - - $addChartTitleParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'ComputerAccountsinAD' - Text = 'Computer Accounts' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) - } - Add-ChartTitle @addChartTitleParams - - $chartFileItem = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) - } - } - if ($OutObj) { - Section -Style Heading4 'Computer Accounts in Domain' { - if ($chartFileItem) { - Image -Text 'Computer Accounts in Domain - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem - } - $OutObj | Table @TableParams - } - } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) - } try { $OutObj = @() $DaysInactive = 90 $dormanttime = (Get-Date).Adddays(-90) $passwordtime = (Get-Date).Adddays(-30) - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $Computers = Invoke-Command -Session $TempPssSession {Get-ADComputer -Server $using:DC -Filter * -Properties *} $Dormant = $Computers | Where-Object {[datetime]::FromFileTime($_.lastlogontimestamp) -lt $dormanttime} $PasswordAge = $Computers | Where-Object {$_.PasswordLastSet -le $passwordtime} - $SidHistory = $Computers | Select-Object -ExpandProperty SIDHistory - $Categories = @('Dormant (> 90 days)','Password Age (> 30 days)','SidHistory') + $SidHistory = $Computers.SIDHistory + $Categories = @('Total Computers', 'Dormant (> 90 days)','Password Age (> 30 days)','SidHistory') if ($Categories) { Write-PscriboMessage "Collecting Status of Computer Accounts." foreach ($Category in $Categories) { try { - if ($Category -eq 'Dormant (> 90 days)') { + if ($Category -eq 'Total Computers') { + $Values = $Computers + } + elseif ($Category -eq 'Dormant (> 90 days)') { $Values = $Dormant } elseif ($Category -eq 'Password Age (> 30 days)') { @@ -672,23 +513,23 @@ function Get-AbrADDomainObject { } $inObj = [ordered] @{ 'Category' = $Category - 'Enabled Count' = ($Values.Enabled).Count + 'Enabled' = ($Values.Enabled -eq $True | Measure-Object).Count 'Enabled %' = Switch ($Computers.Count) { 0 {'0'} $Null {'0'} - default {[math]::Round((($Values.Enabled).Count / $Computers.Count * 100), 0)} + default {[math]::Round((($Values.Enabled -eq $True | Measure-Object).Count / $Computers.Count * 100), 2)} } - 'Disabled Count' = ($Null -eq $Values.Enabled).Count + 'Disabled' = ($Values.Enabled -eq $False | Measure-Object).Count 'Disabled %' = Switch ($Computers.Count) { 0 {'0'} $Null {'0'} - default {[math]::Round((($Null -eq $Values.Enabled).Count / $Computers.Count * 100), 0)} + default {[math]::Round((($Values.Enabled -eq $False | Measure-Object).Count / $Computers.Count * 100), 2)} } - 'Total Count' = ($Values.Enabled).Count + 'Total' = ($Values | Measure-Object).Count 'Total %' = Switch ($Computers.Count) { 0 {'0'} $Null {'0'} - default {[math]::Round((($Values.Enabled).Count / $Computers.Count * 100), 0)} + default {[math]::Round((($Values | Measure-Object).Count / $Computers.Count * 100), 2)} } } @@ -707,52 +548,54 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - try { - $sampleData = $OutObj + if ($Options.EnableCharts) { + try { + $sampleData = $OutObj - $exampleChart = New-Chart -Name StatusofComputerAccounts -Width 600 -Height 400 + $exampleChart = New-Chart -Name StatusofComputerAccounts -Width 600 -Height 400 - $addChartAreaParams = @{ - Chart = $exampleChart - Name = 'exampleChartArea' - } - $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru + $addChartAreaParams = @{ + Chart = $exampleChart + Name = 'exampleChartArea' + } + $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru - $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'Category' - YField = 'Total Count' - Palette = 'Blue' - ColorPerDataPoint = $true - } - $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru + $addChartSeriesParams = @{ + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'exampleChartSeries' + XField = 'Category' + YField = 'Total' + Palette = 'Blue' + ColorPerDataPoint = $true + } + $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru - $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Category' - TitleAlignment = 'Center' - } - Add-ChartLegend @addChartLegendParams + $addChartLegendParams = @{ + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' + } + Add-ChartLegend @addChartLegendParams - $addChartTitleParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'StatusofComputerAccounts' - Text = 'Computer Accounts' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) - } - Add-ChartTitle @addChartTitleParams + $addChartTitleParams = @{ + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'StatusofComputerAccounts' + Text = 'Computer Accounts' + Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + } + Add-ChartTitle @addChartTitleParams - $chartFileItem = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + $chartFileItem = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru + } + catch { + Write-PscriboMessage -IsWarning $($_.Exception.Message) + } } if ($OutObj) { Section -Style Heading4 'Status of Computer Accounts' { - if ($chartFileItem -and ($OutObj.'Total Count' | Measure-Object -Sum).Sum -ne 0) { + if ($chartFileItem -and ($OutObj.'Total' | Measure-Object -Sum).Sum -ne 0) { Image -Text 'Status of Computer Accounts - Diagram' -Align 'Center' -Percent 100 -Path $chartFileItem } $OutObj | Table @TableParams @@ -769,13 +612,12 @@ function Get-AbrADDomainObject { if ($Domain) { Write-PscriboMessage "Collecting Operating Systems in Active Directory." try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $OSObjects = Invoke-Command -Session $TempPssSession {Get-ADComputer -Server $using:DC -Filter "name -like '*'" -Properties operatingSystem | Group-Object -Property operatingSystem | Select-Object Name,Count} + $OSObjects = $Computers | Where-Object {$_.name -like '*' } | Group-Object -Property operatingSystem | Select-Object Name,Count if ($OSObjects) { foreach ($OSObject in $OSObjects) { $inObj = [ordered] @{ 'Operating System' = Switch ([string]::IsNullOrEmpty($OSObject.Name)) { - $True {'Unknown'} + $True {'No OS Specified'} default {$OSObject.Name} } 'Count' = $OSObject.Count @@ -797,6 +639,7 @@ function Get-AbrADDomainObject { $OutObj | Sort-Object -Property 'Operating System' | Table @TableParams if ($HealthCheck.Domain.Security -and ($OutObj | Where-Object {$_.'Operating System' -like '* NT*' -or $_.'Operating System' -like '*2000*' -or $_.'Operating System' -like '*2003*' -or $_.'Operating System' -like '*2008*' -or $_.'Operating System' -like '* NT*' -or $_.'Operating System' -like '*2000*' -or $_.'Operating System' -like '* 95*' -or $_.'Operating System' -like '* 7*' -or $_.'Operating System' -like '* 8 *' -or $_.'Operating System' -like '* 98*' -or $_.'Operating System' -like '*XP*' -or $_.'Operating System' -like '* Vista*'})) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Security Best Practice: Operating systems that are no longer supported for security updates are not maintained or updated for vulnerabilities leaving them open to potential attack. Organizations must transition to a supported operating system to ensure continued support and to increase the organization security posture" -Italic -Bold } } @@ -856,8 +699,8 @@ function Get-AbrADDomainObject { if ($Domain) { foreach ($Item in $Domain) { Write-PscriboMessage "Collecting the Active Directory Fined Grained Password Policies of domain $Item." - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Item | Select-Object -ExpandProperty PDCEmulator} - $PasswordPolicy = Invoke-Command -Session $TempPssSession {Get-ADFineGrainedPasswordPolicy -Server $using:DC -Filter {Name -like "*"} -Properties * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName} | Sort-Object -Property Name + $DCPDC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Item | Select-Object -ExpandProperty PDCEmulator} + $PasswordPolicy = Invoke-Command -Session $TempPssSession {Get-ADFineGrainedPasswordPolicy -Server $using:DCPDC -Filter {Name -like "*"} -Properties * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName} | Sort-Object -Property Name if ($PasswordPolicy) { Section -Style Heading4 'Fined Grained Password Policies' { $FGPPInfo = @() @@ -865,7 +708,7 @@ function Get-AbrADDomainObject { try { $Accounts = @() foreach ($ADObject in $FGPP.AppliesTo) { - $Accounts += Invoke-Command -Session $TempPssSession {Get-ADObject $using:ADObject -Server $using:DC -Properties * | Select-Object -ExpandProperty sAMAccountName } + $Accounts += Invoke-Command -Session $TempPssSession {Get-ADObject $using:ADObject -Server $using:DC -Properties sAMAccountName | Select-Object -ExpandProperty sAMAccountName } } $inObj = [ordered] @{ 'Name' = $FGPP.Name @@ -930,8 +773,8 @@ function Get-AbrADDomainObject { foreach ($Item in $Domain) { Write-PscriboMessage "Collecting the Active Directory LAPS Policies from domain $Item." $DomainInfo = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop} - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Item | Select-Object -ExpandProperty PDCEmulator} - $LAPS = Invoke-Command -Session $TempPssSession {Get-ADObject -Server $using:DC "CN=ms-Mcs-AdmPwd,CN=Schema,CN=Configuration,$(($using:DomainInfo).DistinguishedName)"} | Sort-Object -Property Name + $DCPDC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Item | Select-Object -ExpandProperty PDCEmulator} + $LAPS = Invoke-Command -Session $TempPssSession {Get-ADObject -Server $using:DCPDC "CN=ms-Mcs-AdmPwd,CN=Schema,CN=Configuration,$(($using:DomainInfo).DistinguishedName)"} | Sort-Object -Property Name Section -Style Heading4 'Local Administrator Password Solution' { $LAPSInfo = @() try { @@ -983,6 +826,7 @@ function Get-AbrADDomainObject { if ($HealthCheck.Domain.Security -and ($LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' })) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Security Best Practice: LAPS simplifies password management while helping customers implement additional recommended defenses against cyberattacks. In particular, the solution mitigates the risk of lateral escalation that results when customers use the same administrative local account and password combination on their computers." -Italic -Bold } } @@ -997,7 +841,6 @@ function Get-AbrADDomainObject { if ($Domain) { Write-PScriboMessage "Collecting the Active Directory Group Managed Service Accounts for $Item." try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Item | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} Write-PScriboMessage "Collecting the Active Directory Group Managed Service Accounts from DC $DC." $GMSA = Invoke-Command -Session $TempPssSession {Get-ADServiceAccount -Server $using:DC -Filter * -Properties *} if ($GMSA) { diff --git a/Src/Private/Get-AbrADDuplicateObject.ps1 b/Src/Private/Get-AbrADDuplicateObject.ps1 index e45cda0..357d659 100644 --- a/Src/Private/Get-AbrADDuplicateObject.ps1 +++ b/Src/Private/Get-AbrADDuplicateObject.ps1 @@ -68,6 +68,7 @@ function Get-AbrADDuplicateObject { } $OutObj | Table @TableParams Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Corrective Actions: Ensure there aren't any duplicate object." -Italic -Bold } } diff --git a/Src/Private/Get-AbrADDuplicateSPN.ps1 b/Src/Private/Get-AbrADDuplicateSPN.ps1 index 1034dcd..14439f9 100644 --- a/Src/Private/Get-AbrADDuplicateSPN.ps1 +++ b/Src/Private/Get-AbrADDuplicateSPN.ps1 @@ -68,6 +68,7 @@ function Get-AbrADDuplicateSPN { $OutObj | Sort-Object -Property 'Name' | Table @TableParams if ($HealthCheck.Domain.SPN) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Corrective Actions: Ensure there aren't any duplicate SPNs (other than krbtgt)." -Italic -Bold } } diff --git a/Src/Private/Get-AbrADFSMO.ps1 b/Src/Private/Get-AbrADFSMO.ps1 index c71dd6a..d72238b 100644 --- a/Src/Private/Get-AbrADFSMO.ps1 +++ b/Src/Private/Get-AbrADFSMO.ps1 @@ -5,7 +5,7 @@ function Get-AbrADFSMO { .DESCRIPTION .NOTES - Version: 0.7.6 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -37,11 +37,11 @@ function Get-AbrADFSMO { try { Write-PscriboMessage "Discovered Active Directory FSMO information of domain $Domain." $inObj = [ordered] @{ - 'Infrastructure Master Server' = $DomainData.InfrastructureMaster - 'RID Master Server' = $DomainData.RIDMaster + 'Infrastructure Master' = $DomainData.InfrastructureMaster + 'RID Master' = $DomainData.RIDMaster 'PDC Emulator Name' = $DomainData.PDCEmulator - 'Domain Naming Master Server' = $ForestData.DomainNamingMaster - 'Schema Master Server' = $ForestData.SchemaMaster + 'Domain Naming Master' = $ForestData.DomainNamingMaster + 'Schema Master' = $ForestData.SchemaMaster } $OutObj += [pscustomobject]$inobj } diff --git a/Src/Private/Get-AbrADForest.ps1 b/Src/Private/Get-AbrADForest.ps1 index 170d1f6..e82cb6a 100644 --- a/Src/Private/Get-AbrADForest.ps1 +++ b/Src/Private/Get-AbrADForest.ps1 @@ -5,7 +5,7 @@ function Get-AbrADForest { .DESCRIPTION .NOTES - Version: 0.7.2 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -30,6 +30,8 @@ function Get-AbrADForest { $DomainDN = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity (Get-ADForest | Select-Object -ExpandProperty RootDomain )).DistinguishedName} $TombstoneLifetime = Invoke-Command -Session $TempPssSession {Get-ADObject "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,$using:DomainDN" -Properties tombstoneLifetime | Select-Object -ExpandProperty tombstoneLifetime} $ADVersion = Invoke-Command -Session $TempPssSession {Get-ADObject (Get-ADRootDSE).schemaNamingContext -property objectVersion | Select-Object -ExpandProperty objectVersion} + $ValuedsHeuristics = Invoke-Command -Session $TempPssSession {Get-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,$(($using:DomainDN))" -Properties dsHeuristics -ErrorAction SilentlyContinue} + If ($ADVersion -eq '88') {$server = 'Windows Server 2019'} ElseIf ($ADVersion -eq '87') {$server = 'Windows Server 2016'} ElseIf ($ADVersion -eq '69') {$server = 'Windows Server 2012 R2'} @@ -56,7 +58,14 @@ function Get-AbrADForest { 'Application Partitions' = $Item.ApplicationPartitions 'PartitionsContainer' = [string]$Item.PartitionsContainer 'SPN Suffixes' = ConvertTo-EmptyToFiller $Item.SPNSuffixes - 'UPN Suffixes' = ConvertTo-EmptyToFiller $Item.UPNSuffixes + 'UPN Suffixes' = ConvertTo-EmptyToFiller ($Item.UPNSuffixes -join ', ') + 'Anonymous Access (dsHeuristics)' = &{ + if (($ValuedsHeuristics.dsHeuristics -eq "") -or ($ValuedsHeuristics.dsHeuristics.Length -lt 7)) { + "Disabled" + } elseif (($ValuedsHeuristics.dsHeuristics.Length -ge 7) -and ($ValuedsHeuristics.dsHeuristics[6] -eq "2")) { + "Enabled" + } + } } $OutObj += [pscustomobject]$inobj } @@ -65,6 +74,10 @@ function Get-AbrADForest { } } + if ($HealthCheck.Domain.Security) { + $OutObj | Where-Object { $_.'Anonymous Access (dsHeuristics)' -eq 'Enabled'} | Set-Style -Style Warning -Property 'Anonymous Access (dsHeuristics)' + } + $TableParams = @{ Name = "Forest Summary - $($ForestInfo)" List = $true @@ -74,13 +87,108 @@ function Get-AbrADForest { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Table @TableParams + if ($HealthCheck.Domain.Security -and ($OutObj | Where-Object { $_.'Anonymous Access (dsHeuristics)' -eq 'Enabled'}) ) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Best Practice: Anonymous Access to Active Directory forest data above the rootDSE level must be disabled." -Italic -Bold + Paragraph "Reference:" -Italic -Bold -Underline + Paragraph "https://www.stigviewer.com/stig/active_directory_forest/2016-02-19/finding/V-8555" -Bold + } + } + } + catch { + Write-PscriboMessage -IsWarning $_.Exception.Message + } + try { + Section -Style Heading3 'Certificate Authority' { + if ($Options.ShowDefinitionInfo) { + Paragraph 'In cryptography, a certificate authority or certification authority (CA) is an entity that issues digital certificates. A digital certificate certifies the ownership of a public key by the named subject of the certificate. This allows others (relying parties) to rely upon signatures or on assertions made about the private key that corresponds to the certified public key. A CA acts as a trusted third party trusted both by the subject (owner) of the certificate and by the party relying upon the certificate. The format of these certificates is specified by the X.509 or EMV standard.' + BlankLine + } + if (!$Options.ShowDefinitionInfo) { + Paragraph "The following section provides a summary of the Active Directory PKI Infrastructure Information." + BlankLine + } + Write-PscriboMessage "Discovering certificate authority information on forest $ForestInfo." + $ConfigNCDN = $Data.PartitionsContainer.Split(',') | Select-Object -Skip 1 + $rootCA = Get-ADObjectSearch -DN "CN=Certification Authorities,CN=Public Key Services,CN=Services,$($ConfigNCDN -join ',')" -Filter { objectClass -eq "certificationAuthority" } -Properties "Name" -SelectPrty 'DistinguishedName','Name' -Session $TempPssSession + if ($rootCA) { + Section -ExcludeFromTOC -Style NOTOCHeading4 'Certification Authority Root(s)' { + $OutObj = @() + Write-PscriboMessage "Discovered Certificate Authority Information on forest $ForestInfo." + foreach ($Item in $rootCA) { + try { + Write-PscriboMessage "Collecting Certificate Authority Information '$($Item.Name)'" + $inObj = [ordered] @{ + 'Name' = $Item.Name + 'Distinguished Name' = $Item.DistinguishedName + } + $OutObj += [pscustomobject]$inobj + } + catch { + Write-PscriboMessage -IsWarning $_.Exception.Message + } + } + + if ($HealthCheck.Forest.BestPractice) { + ($OutObj | Measure-Object).Count -gt 1 | Set-Style -Style Warning + } + + $TableParams = @{ + Name = "Certificate Authority Root(s) - $($ForestInfo)" + List = $false + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + if ($HealthCheck.Forest.BestPractice -and (($OutObj | Measure-Object).Count -gt 1 ) ) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Best Practice: In most PKI implementations, it is not typical to have multiple Root CAs. Its recommended a detailed review of the current PKI infrastructure and Root CA requirements." -Italic -Bold + } + } + } + Write-PscriboMessage "Discovering certificate authority issuers on forest $ForestInfo." + $ConfigNCDN = $Data.PartitionsContainer.Split(',') | Select-Object -Skip 1 + $subordinateCA = Get-ADObjectSearch -DN "CN=Enrollment Services,CN=Public Key Services,CN=Services,$($ConfigNCDN -join ',')" -Filter { objectClass -eq "pKIEnrollmentService" } -Properties "*" -SelectPrty 'dNSHostName','Name' -Session $TempPssSession + if ($subordinateCA) { + Section -ExcludeFromTOC -Style NOTOCHeading4 'Certification Authority Issuer(s)' { + $OutObj = @() + Write-PscriboMessage "Discovered Certificate Authority issuers on forest $ForestInfo." + foreach ($Item in $subordinateCA) { + try { + Write-PscriboMessage "Collecting Certificate Authority issuers '$($Item.Name)'" + $inObj = [ordered] @{ + 'Name' = $Item.Name + 'DNS Name' = $Item.dNSHostName + } + $OutObj += [pscustomobject]$inobj + } + catch { + Write-PscriboMessage -IsWarning $_.Exception.Message + } + } + + $TableParams = @{ + Name = "Certificate Authority Issuer(s) - $($ForestInfo)" + List = $false + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + } + } } } catch { Write-PscriboMessage -IsWarning $_.Exception.Message } try { - Section -Style Heading5 'Optional Features' { + Section -Style Heading3 'Optional Features' { Write-PscriboMessage "Discovering Optional Features enabled on forest $ForestInfo." $Data = Invoke-Command -Session $TempPssSession {Get-ADOptionalFeature -Filter *} $OutObj = @() @@ -104,6 +212,10 @@ function Get-AbrADForest { } } + if ($HealthCheck.Forest.BestPractice) { + $OutObj | Where-Object { $_.'Name' -eq 'Recycle Bin Feature' -and $_.'Enabled' -eq 'No'} | Set-Style -Style Warning -Property 'Enabled' + } + $TableParams = @{ Name = "Optional Features - $($ForestInfo)" List = $false @@ -113,6 +225,15 @@ function Get-AbrADForest { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams + if ($HealthCheck.Forest.BestPractice -and ($OutObj | Where-Object { $_.'Name' -eq 'Recycle Bin Feature' -and $_.'Enabled' -eq 'No'}) ) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Best Practice: Accidental deletion of Active Directory objects is common for Active Directory Domain Services (AD DS) users. With the Recycle Bin Feature, one could recover accidentally deleted objects in Active Directory. Enable the Recycle Bin feature for the forest." -Italic -Bold + BlankLine + Paragraph "Reference:" -Italic -Bold -Underline + BlankLine + Paragraph "https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-ad-recycle-bin-understanding-implementing-best-practices-and/ba-p/396944" -Bold + } } } } diff --git a/Src/Private/Get-AbrADGPO.ps1 b/Src/Private/Get-AbrADGPO.ps1 index b887549..0622257 100644 --- a/Src/Private/Get-AbrADGPO.ps1 +++ b/Src/Private/Get-AbrADGPO.ps1 @@ -5,7 +5,7 @@ function Get-AbrADGPO { .DESCRIPTION .NOTES - Version: 0.7.11 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -40,11 +40,18 @@ function Get-AbrADGPO { try { foreach ($GPO in $GPOs) { try { - Write-PscriboMessage "Collecting Active Directory Group Policy Objects '$($GPO.DisplayName)'. (Group Policy Objects)" + Write-PscriboMessage "Collecting Active Directory Group Policy Objects '$($GPO.DisplayName)'." $inObj = [ordered] @{ 'GPO Name' = $GPO.DisplayName 'GPO Status' = ($GPO.GpoStatus -creplace '([A-Z\W_]|\d+)(?=30 days and PasswordLastSet >= 365 days) on Domain $($Domain.ToString().ToUpper())" + BlankLine + $OutObj = @() + Write-PscriboMessage "Collecting Inactive Privileged Accounts information from $($Domain)." + foreach ($InactivePrivilegedUser in $InactivePrivilegedUsers) { + try { + $inObj = [ordered] @{ + 'Username' = $InactivePrivilegedUser.SamAccountName + 'Created' = Switch ($InactivePrivilegedUser.Created) { + $Null {'--'} + default {$InactivePrivilegedUser.Created.ToShortDateString()} + } + 'Password Last Set' = Switch ($InactivePrivilegedUser.PasswordLastSet) { + $Null {'--'} + default {$InactivePrivilegedUser.PasswordLastSet.ToShortDateString()} + } + 'Last Logon Date' = Switch ($InactivePrivilegedUser.LastLogonDate) { + $Null {'--'} + default {$InactivePrivilegedUser.LastLogonDate.ToShortDateString()} + } + } + $OutObj += [pscustomobject]$inobj + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Inactive Privileged Accounts Item)" + } + } + + if ($HealthCheck.Domain.Security) { + $OutObj | Set-Style -Style Warning + } + + $TableParams = @{ + Name = "Inactive Privileged Accounts - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 40, 20, 20, 20 + } + + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Table @TableParams + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Corrective Actions: Unused or underutilized accounts in highly privileged groups, outside of any break-glass emergency accounts like the default Administrator account, should have their AD Admin privileges removed." -Italic -Bold + } + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Account Security Assessment Table)" + } try { $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} - $UserSPNs = Invoke-Command -Session $TempPssSession {Get-ADUser -Server $using:Domain -filter {ServicePrincipalName -like '*'} -Properties PasswordLastSet,LastLogonDate,ServicePrincipalName,TrustedForDelegation,TrustedtoAuthForDelegation} + $UserSPNs = Invoke-Command -Session $TempPssSession {Get-ADUser -ResultPageSize 1000 -Server $using:Domain -filter {ServicePrincipalName -like '*'} -Properties PasswordLastSet,LastLogonDate,ServicePrincipalName,TrustedForDelegation,TrustedtoAuthForDelegation} Write-PscriboMessage "Discovered Service Accounts information from $Domain." if ($UserSPNs) { Section -ExcludeFromTOC -Style NOTOCHeading5 'Service Accounts Assessment' { @@ -217,11 +272,11 @@ function Get-AbrADSecurityAssessment { 'Username' = $UserSPN.SamAccountName 'Enabled' = ConvertTo-TextYN $UserSPN.Enabled 'Password Last Set' = Switch ($UserSPN.PasswordLastSet) { - $Null {'-'} + $Null {'--'} default {$UserSPN.PasswordLastSet.ToShortDateString()} } 'Last Logon Date' = Switch ($UserSPN.LastLogonDate) { - $Null {'-'} + $Null {'--'} default {$UserSPN.LastLogonDate.ToShortDateString()} } 'Service Principal Name' = $UserSPN.ServicePrincipalName @@ -244,6 +299,7 @@ function Get-AbrADSecurityAssessment { } $OutObj | Table @TableParams Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Corrective Actions: Service accounts are that gray area between regular user accounts and admin accounts that are often highly privileged. They are almost always over-privileged due to documented vendor requirements or because of operational challenges. Ensure there aren't any account with weak security posture." -Italic -Bold } } diff --git a/Src/Private/Get-AbrADSite.ps1 b/Src/Private/Get-AbrADSite.ps1 index 1b3c48c..557dc4e 100644 --- a/Src/Private/Get-AbrADSite.ps1 +++ b/Src/Private/Get-AbrADSite.ps1 @@ -5,7 +5,7 @@ function Get-AbrADSite { .DESCRIPTION .NOTES - Version: 0.7.0 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -26,7 +26,7 @@ function Get-AbrADSite { try { $Site = Invoke-Command -Session $TempPssSession {Get-ADReplicationSite -Filter * -Properties *} if ($Site) { - Section -Style Heading3 'Domain Sites' { + Section -Style Heading3 'Sites' { $OutObj = @() Write-PscriboMessage "Discovered Active Directory Sites information of forest $ForestInfo" foreach ($Item in $Site) { @@ -42,16 +42,27 @@ function Get-AbrADSite { 'Site Name' = $Item.Name 'Description' = ConvertTo-EmptyToFiller $Item.Description 'Subnets' = Switch (($SubnetArray).count) { - 0 {"--"} + 0 {"No subnet assigned"} default {$SubnetArray} } - 'Creation Date' = $Item.createTimeStamp.ToShortDateString() + 'Domain Controllers' = &{ + $ServerArray = @() + $Servers = try {Get-ADObjectSearch -DN "CN=Servers,$($Item.DistinguishedName)" -Filter { objectClass -eq "Server" } -Properties "DNSHostName" -SelectPrty 'DNSHostName','Name' -Session $TempPssSession} catch {'Unknown'} + foreach ($Object in $Servers) { + $ServerArray += $Object.Name + } + + if ($ServerArray) { + return $ServerArray + } else {'No DC assigned'} + } } $OutObj += [pscustomobject]$inobj if ($HealthCheck.Site.BestPractice) { - $OutObj | Where-Object { $_.'Subnets' -eq '-'} | Set-Style -Style Warning -Property 'Subnets' - $OutObj | Where-Object { $_.'Description' -eq '-'} | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Subnets' -eq 'No subnet assigned'} | Set-Style -Style Warning -Property 'Subnets' + $OutObj | Where-Object { $_.'Description' -eq '--'} | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Domain Controllers' -eq 'No DC assigned'} | Set-Style -Style Warning -Property 'Domain Controllers' } } catch { @@ -62,21 +73,76 @@ function Get-AbrADSite { $TableParams = @{ Name = "Sites - $($ForestInfo)" List = $false - ColumnWidths = 25, 30, 25, 20 + ColumnWidths = 25, 30, 20, 25 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Site Name' | Table @TableParams - if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Subnets' -eq '-'}) -or ($OutObj | Where-Object { $_.'Description' -eq '-'}))) { + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Subnets' -eq '--'}) -or ($OutObj | Where-Object { $_.'Description' -eq '--'}))) { Paragraph "Health Check:" -Italic -Bold -Underline - if ($OutObj | Where-Object { $_.'Subnets' -eq '-'}) { + BlankLine + if ($OutObj | Where-Object { $_.'Subnets' -eq 'No subnet assigned'}) { Paragraph "Corrective Actions: Ensure Sites have an associated subnet. If subnets are not associated with AD Sites users in the AD Sites might choose a remote domain controller for authentication which in turn might result in excessive use of a remote domain controller." -Italic -Bold } - if ($OutObj | Where-Object { $_.'Description' -eq '-'}) { - Paragraph "Best Practices: Ensure Sites have a defined description." -Italic -Bold + if ($OutObj | Where-Object { $_.'Description' -eq '--'}) { + BlankLine + Paragraph "Best Practice: It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." -Italic -Bold + } + } + try { + $Replications = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADReplicationConnection -Properties * -Filter *} + if ($Replications) { + Section -Style Heading4 'Connection Objects' { + $OutObj = @() + Write-PscriboMessage "Discovered Connection Objects information of forest $ForestInfo" + foreach ($Repl in $Replications) { + try { + $inObj = [ordered] @{ + 'Name' = &{ + if ($Repl.AutoGenerated) { + "" + } else { + $Repl.Name + } + } + 'From Server' = $Repl.ReplicateFromDirectoryServer.Split(",")[1].SubString($Repl.ReplicateFromDirectoryServer.Split(",")[1].IndexOf("=")+1) + 'To Server' = $Repl.ReplicateToDirectoryServer.Split(",")[0].SubString($Repl.ReplicateToDirectoryServer.Split(",")[0].IndexOf("=")+1) + 'From Site' = $Repl.fromserver.Split(",")[3].SubString($Repl.fromserver.Split(",")[3].IndexOf("=")+1) + } + $OutObj += [pscustomobject]$inobj + + if ($HealthCheck.Site.Replication) { + $OutObj | Where-Object { $_.'Name' -ne ''} | Set-Style -Style Warning -Property 'Name' + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Site Replication Connection Item)" + } + } + + $TableParams = @{ + Name = "Connection Objects - $($ForestInfo)" + List = $false + ColumnWidths = 25, 25, 25, 25 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'From Site' | Table @TableParams + if ($HealthCheck.Site.BestPractice -and ($OutObj | Where-Object { $_.'Name' -ne ''})) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'Name' -ne ''}) { + Paragraph "Best Practice: By default, the replication topology is managed automatically and optimizes existing connections. However, manual connections created by an administrator are not modified or optimized. Verify that all topology information is entered for Site Links and delete all manual connection objects." -Italic -Bold + } + } + } } } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Connection Objects)" + } try { $Subnet = Invoke-Command -Session $TempPssSession {Get-ADReplicationSubnet -Filter * -Properties *} if ($Subnet) { @@ -89,13 +155,17 @@ function Get-AbrADSite { $inObj = [ordered] @{ 'Subnet' = $Item.Name 'Description' = ConvertTo-EmptyToFiller $Item.Description - 'Sites' = Get-ADObject $Item.Site | Select-Object -ExpandProperty Name - 'Creation Date' = $Item.Created.ToShortDateString() + 'Sites' = &{ + try { + $Item.Site.Split(",")[0].SubString($Item.Site.Split(",")[0].IndexOf("=")+1) + } catch {"No site assigned"} + } } $OutObj += [pscustomobject]$inObj if ($HealthCheck.Site.BestPractice) { - $OutObj | Where-Object { $_.'Description' -eq '-'} | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Description' -eq '--'} | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Sites' -eq 'No site assigned'} | Set-Style -Style Warning -Property 'Sites' } } catch { @@ -106,15 +176,89 @@ function Get-AbrADSite { $TableParams = @{ Name = "Site Subnets - $($ForestInfo)" List = $false - ColumnWidths = 20, 30, 35, 15 + ColumnWidths = 20, 40, 40 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Subnet' | Table @TableParams - if ($HealthCheck.Site.BestPractice -and ($OutObj | Where-Object { $_.'Description' -eq '-'})) { + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Description' -eq '--'}) -or ($OutObj | Where-Object { $_.'Sites' -eq 'No site assigned'}))) { Paragraph "Health Check:" -Italic -Bold -Underline - Paragraph "Best Practices: Ensure that subnets has a defined description." -Italic -Bold + BlankLine + if ($OutObj | Where-Object { $_.'Description' -eq '--'}) { + Paragraph "Best Practice: It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." -Italic -Bold + BlankLine + } + if ($OutObj | Where-Object { $_.'Sites' -eq 'No site assigned'}) { + Paragraph "Corrective Actions: Ensure Subnet have an associated site. If subnets are not associated with AD Sites users in the AD Sites might choose a remote domain controller for authentication which in turn might result in excessive use of a remote domain controller." -Italic -Bold + } + } + if ($HealthCheck.Site.BestPractice) { + try { + $OutObj = @() + foreach ($Domain in $ADSystem.Domains | Where-Object {$_ -notin $Options.Exclude.Domains}) { + Write-PscriboMessage "Discovered Missing Subnet in AD information from $Domain." + $DomainInfo = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop} + foreach ($DC in ($DomainInfo.ReplicaDirectoryServers | Where-Object {$_ -notin $Options.Exclude.DCs})) { + try { + $DCPssSession = New-PsSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $Path = "\\$DC\admin`$\debug\netlogon.log" + if ((Invoke-Command -Session $DCPssSession {Test-Path -Path $using:path}) -and (Invoke-Command -Session $DCPssSession {(Get-Content -Path $using:path | Measure-Object -Line).lines -gt 0})) { + Write-PscriboMessage "Collecting Missing Subnet in AD information from $($Domain)." + $NetLogonContents = Invoke-Command -Session $DCPssSession { (Get-Content -Path $using:Path)[-200..-1] } + foreach ($Line in $NetLogonContents) { + if ($Line -match "NO_CLIENT_SITE") { + $inObj = [ordered] @{ + 'DC' = $DC + 'IP' = $Line.Split(":")[4].trim(" ").Split(" ")[1] + } + + $OutObj += [pscustomobject]$inobj + } + + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Replication Status' -eq 'Normal' } | Set-Style -Style OK -Property 'Replication Status' + } + } + } else { + Write-PScriboMessage "Unable to read $Path on $DC" + } + if ($DCPssSession) { + Remove-PsSession -Session $DCPssSession + } + } + catch { + Write-PscriboMessage -IsWarning "Missing Subnet in AD Item Section: $($_.Exception.Message)" + } + } + } + if ($OutObj) { + Section -Style Heading4 'Missing Subnets in AD' { + Paragraph "The following table list the NO_CLIENT_SITE entries found in the netlogon.log file at each DC in the forest." + Blankline + $TableParams = @{ + Name = "Missing Subnets - $($ForestInfo)" + List = $false + ColumnWidths = 40, 60 + } + + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + + $OutObj | Sort-Object -Property 'DC','IP' | Get-Unique -AsString | Table @TableParams + if ($HealthCheck.Site.BestPractice) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Corrective Actions: Make sure that all the subnets at each Site are properly defined. Missing subnets can cause clients to not use the site's local DCs." -Italic -Bold + BlankLine + } + } + } + } + catch { + Write-PscriboMessage -IsWarning "Sysvol Replication Table Section: $($_.Exception.Message)" + } } } } @@ -142,29 +286,145 @@ function Get-AbrADSite { 'Cost' = $Item.Cost 'Replication Frequency' = "$($Item.ReplicationFrequencyInMinutes) min" 'Transport Protocol' = $Item.InterSiteTransportProtocol - 'Sites' = $SiteArray + 'Options' = Switch ($Item.Options) { + $null {'Change Notification is Disabled'} + '0' {'(0)Change Notification is Disabled'} + '1' {'(1)Change Notification is Enabled with Compression'} + '2' {'(2)Force sync in opposite direction at end of sync'} + '3' {'(3)Change Notification is Enabled with Compression and Force sync in opposite direction at end of sync'} + '4' {'(4)Disable compression of Change Notification messages'} + '5' {'(5)Change Notification is Enabled without Compression'} + '6' {'(6)Force sync in opposite direction at end of sync and Disable compression of Change Notification messages'} + '7' {'(7)Change Notification is Enabled without Compression and Force sync in opposite direction at end of sync'} + Default {"Unknown siteLink option: $($Item.Options)"} + } + 'Sites' = $SiteArray -join "; " + 'Protected From Accidental Deletion' = ConvertTo-TextYN $Item.ProtectedFromAccidentalDeletion + 'Description' = ConvertTo-EmptyToFiller $Item.Description } - $OutObj += [pscustomobject]$inobj + $OutObj = [pscustomobject]$inobj + + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Description' -eq '--'} | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' } | Set-Style -Style Warning -Property 'Options' + $OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No'} | Set-Style -Style Warning -Property 'Protected From Accidental Deletion' + } + + $TableParams = @{ + Name = "Site Links - $($Item.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Site Link Name' | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No'}) -or (($OutObj | Where-Object { $_.'Description' -eq '--'}) -or ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' })))) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'Description' -eq '--'}) { + Paragraph "Best Practice: It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." -Italic -Bold + BlankLine + } + if ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' }) { + Paragraph "Best Practice: Enabling change notification treats an INTER-site replication connection like an INTRA-site connection. Replication between sites with change notification is almost instant. Microsoft recommends using an Option number value of 5 (Change Notification is Enabled without Compression)." -Italic -Bold + BlankLine + } + if ($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No'}) { + Paragraph "Best Practice: If the Site Links in your Active Directory are not protected from accidental deletion, your environment can experience disruptions that might be caused by accidental bulk deletion of objects." -Italic -Bold + BlankLine + } + BlankLine + } + } catch { Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Site Links)" } } + } + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Site Subnets)" + } + try { + $OutObj = @() + foreach ($Domain in $ADSystem.Domains | Where-Object {$_ -notin $Options.Exclude.Domains}) { + Write-PscriboMessage "Discovered AD Domain Sysvol Replication information from $Domain." + $DomainInfo = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop} + foreach ($DC in ($DomainInfo.ReplicaDirectoryServers | Where-Object {$_ -notin $Options.Exclude.DCs})) { + $DCCIMSession = New-CIMSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $Replication = Get-CIMInstance -CIMSession $DCCIMSession -Namespace "root/microsoftdfs" -Class "dfsrreplicatedfolderinfo" -Filter "ReplicatedFolderName = 'SYSVOL Share'" -EA 0 -Verbose:$False | Select-Object State + if ($DCCIMSession) { + Remove-CimSession -CimSession $DCCIMSession + } + + try { + Write-PscriboMessage "Collecting Sysvol Replication information from $($Domain)." + $inObj = [ordered] @{ + 'DC Name' = $DC.split(".", 2)[0] + 'Replication Status' = Switch ($Replication.State) { + 0 {'Uninitialized'} + 1 {'Initialized'} + 2 {'Initial synchronization'} + 3 {'Auto recovery'} + 4 {'Normal'} + 5 {'In error state'} + 6 {'Disabled'} + 7 {'Unknown'} + } + 'Domain' = $Domain + } + $OutObj += [pscustomobject]$inobj + } + catch { + Write-PscriboMessage -IsWarning "Sysvol Replication Item Section: $($_.Exception.Message)" + } + if ($HealthCheck.Site.BestPractice) { + $ReplicationStatusError = @( + 'Uninitialized', + 'Auto recovery', + 'In error state', + 'Disabled', + 'Unknown' + ) + $ReplicationStatusWarn = @( + 'Initialized', + 'Initial synchronization' + ) + $OutObj | Where-Object { $_.'Replication Status' -eq 'Normal' } | Set-Style -Style OK -Property 'Replication Status' + $OutObj | Where-Object { $_.'Replication Status' -in $ReplicationStatusError } | Set-Style -Style Critical -Property 'Replication Status' + $OutObj | Where-Object { $_.'Replication Status' -in $ReplicationStatusWarn } | Set-Style -Style Warning -Property 'Replication Status' + } + + } + } + if ($OutObj) { + Section -Style Heading4 'Sysvol Replication' { $TableParams = @{ - Name = "Site Links - $($ForestInfo)" + Name = "Sysvol Replication - $($Domain.ToString().ToUpper())" List = $false - ColumnWidths = 30, 15, 15, 15, 25 + ColumnWidths = 33, 33, 34 } + if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - $OutObj | Sort-Object -Property 'Site Link Name' | Table @TableParams + + $OutObj | Sort-Object -Property 'Domain' | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Identical Count' -like 'No' }) -or ($OutObj | Where-Object { $_.'Replication Status' -in $ReplicationStatusError }))) { + Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine + Paragraph "Corrective Actions: SYSVOL is a special directory that resides on each domain controller (DC) within a domain. The directory comprises folders that store Group Policy objects (GPOs) and logon scripts that clients need to access and synchronize between DCs. For these logon scripts and GPOs to function properly, SYSVOL should be replicated accurately and rapidly throughout the domain. Ensure that proper SYSVOL replication is in place to ensure identical GPO/SYSVOL content for the domain controller across all Active Directory domains." -Italic -Bold + BlankLine + } } } } catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Site Subnets)" + Write-PscriboMessage -IsWarning "Sysvol Replication Table Section: $($_.Exception.Message)" } } } diff --git a/Src/Private/Get-AbrADSiteReplication.ps1 b/Src/Private/Get-AbrADSiteReplication.ps1 index 4147a81..faac63d 100644 --- a/Src/Private/Get-AbrADSiteReplication.ps1 +++ b/Src/Private/Get-AbrADSiteReplication.ps1 @@ -5,7 +5,7 @@ function Get-AbrADSiteReplication { .DESCRIPTION .NOTES - Version: 0.7.11 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -43,10 +43,18 @@ function Get-AbrADSiteReplication { foreach ($Repl in $Replication) { try { $inObj = [ordered] @{ + 'Name' = &{ + if ($Repl.AutoGenerated) { + "" + } else { + $Repl.Name + } + } + 'From Site' = $Repl.fromserver.Split(",")[3].SubString($Repl.fromserver.Split(",")[3].IndexOf("=")+1) 'GUID' = $Repl.ObjectGUID 'Description' = ConvertTo-EmptyToFiller $Repl.Description - 'Replicate From Directory Server' = ConvertTo-ADObjectName $Repl.ReplicateFromDirectoryServer.Split(",", 2)[1] -Session $TempPssSession -DC $DC - 'Replicate To Directory Server' = ConvertTo-ADObjectName $Repl.ReplicateToDirectoryServer -Session $TempPssSession -DC $DC + 'From Server' = ConvertTo-ADObjectName $Repl.ReplicateFromDirectoryServer.Split(",", 2)[1] -Session $TempPssSession -DC $DC + 'To Server' = ConvertTo-ADObjectName $Repl.ReplicateToDirectoryServer -Session $TempPssSession -DC $DC 'Replicated Naming Contexts' = $Repl.ReplicatedNamingContexts 'Transport Protocol' = $Repl.InterSiteTransportProtocol 'Auto Generated' = ConvertTo-TextYN $Repl.AutoGenerated @@ -57,6 +65,7 @@ function Get-AbrADSiteReplication { if ($HealthCheck.Site.Replication) { $ReplInfo | Where-Object { $_.'Enabled' -ne 'Yes'} | Set-Style -Style Warning -Property 'Enabled' + $ReplInfo | Where-Object { $_.'Auto Generated' -ne 'Yes'} | Set-Style -Style Warning -Property 'Auto Generated' } } catch { @@ -76,9 +85,9 @@ function Get-AbrADSiteReplication { Paragraph "The following section provides detailed information about Site Replication Connection." BlankLine foreach ($Repl in ($ReplInfo | Sort-Object -Property 'Replicate From Directory Server')) { - Section -Style NOTOCHeading5 -ExcludeFromTOC "From: $($Repl.'Replicate From Directory Server') To: $($Repl.'Replicate To Directory Server')" { + Section -Style NOTOCHeading5 -ExcludeFromTOC "Site: $($Repl.'From Site'): From: $($Repl.'From Server') To: $($Repl.'To Server')" { $TableParams = @{ - Name = "Site Replication - $($Repl.'Replicate To Directory Server')" + Name = "Site Replication - $($Repl.'To Server')" List = $true ColumnWidths = 40, 60 } @@ -91,13 +100,13 @@ function Get-AbrADSiteReplication { } } else { Section -Style Heading5 'Sites Replication Connection' { - Paragraph "The following section provide summary information about Site Replication Connection." + Paragraph "The following section provide connection objects to source server ." BlankLine $TableParams = @{ Name = "Site Replication - $($Domain.ToString().ToUpper())" List = $false - Columns = 'Replicate From Directory Server', 'Replicate To Directory Server', 'Transport Protocol', 'Auto Generated', 'Enabled' - ColumnWidths = 28, 27, 15, 15, 15 + Columns = 'Name', 'From Server', 'From Site' + ColumnWidths = 33, 33, 34 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" @@ -125,13 +134,13 @@ function Get-AbrADSiteReplication { foreach ($Status in $RepStatus) { try { $inObj = [ordered] @{ - 'Source DSA' = $Status.'Source DSA' - 'Destination DSA' = $Status.'Destination DSA' - 'Source DSA Site' = $Status.'Source DSA Site' + 'From Server' = $Status.'Source DSA' + 'To Server' = $Status.'Destination DSA' + 'From Site' = $Status.'Source DSA Site' 'Last Success Time' = $Status.'Last Success Time' 'Last Failure Status' = $Status.'Last Failure Status' 'Last Failure Time' = $Status.'Last Failure Time' - 'Number of failures' = $Status.'Number of Failures' + 'Failures' = $Status.'Number of Failures' } $OutObj += [pscustomobject]$inobj @@ -155,11 +164,15 @@ function Get-AbrADSiteReplication { $OutObj| Sort-Object -Property 'Source DSA' | Table @TableParams if ($HealthCheck.Site.Replication -and ($OutObj | Where-Object {$_.'Last Failure Status' -gt 0})) { Paragraph "Health Check:" -Italic -Bold -Underline + BlankLine Paragraph "Best Practices: Replication failure can lead to object inconsistencies and major problems in Active Directory." -Italic -Bold BlankLine } } } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } } } catch { diff --git a/Src/Private/SharedUtilsFunctions.ps1 b/Src/Private/SharedUtilsFunctions.ps1 index 58b2fff..13a2607 100644 --- a/Src/Private/SharedUtilsFunctions.ps1 +++ b/Src/Private/SharedUtilsFunctions.ps1 @@ -212,6 +212,38 @@ function ConvertTo-ADObjectName { return $ADObject; }# end +function Get-ADObjectSearch { + <# + .SYNOPSIS + Used by As Built Report to lookup Object subtree in Active Directory. + .DESCRIPTION + + .NOTES + Version: 0.1.0 + Author: Jonathan Colon + + .EXAMPLE + + .LINK + + #> + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + $DN, + $Session, + $Filter, + $Properties="*", + $SelectPrty + + ) + $ADObject = @() + foreach ($Object in $DN) { + $ADObject += Invoke-Command -Session $Session {Get-ADObject -SearchBase $using:DN -SearchScope OneLevel -Filter $using:Filter -Properties $using:Properties -EA 0 | Select-Object $using:SelectPrty } + } + return $ADObject; +}# end + function ConvertTo-ADCanonicalName { <# .SYNOPSIS diff --git a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 index 6155fcf..d90f1fe 100644 --- a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 +++ b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 @@ -5,7 +5,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { .DESCRIPTION Documents the configuration of Microsoft AD in Word/HTML/Text formats using PScribo. .NOTES - Version: 0.7.12 + Version: 0.7.13 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -85,7 +85,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { Write-PScriboMessage "Connecting to Domain Controller Server '$System'." $script:TempPssSession = New-PSSession $System -Credential $Credential -Authentication $Options.PSDefaultAuthentication -ErrorAction Stop $script:TempCIMSession = New-CIMSession $System -Credential $Credential -Authentication $Options.PSDefaultAuthentication -ErrorAction Stop - $ADSystem = Invoke-Command -Session $TempPssSession { Get-ADForest -ErrorAction Stop} + $script:ADSystem = Invoke-Command -Session $TempPssSession { Get-ADForest -ErrorAction Stop} } Catch { throw "Unable to connect to the Domain Controller: $System" } @@ -149,8 +149,15 @@ function Invoke-AsBuiltReport.Microsoft.AD { foreach ($Domain in $OrderedDomains.split(" ")) { if ($Domain) { + # Define Filter option for Domain variable + if ($Options.Include.Domains) { + $DomainFilterOption = $Domain -in $Options.Include.Domains + + } else { + $DomainFilterOption = $Domain -notin $Options.Exclude.Domains + } try { - if (($Domain -notin $Options.Exclude.Domains ) -and (Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain})) { + if (( $DomainFilterOption ) -and (Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain})) { Section -Style Heading3 "$($Domain.ToString().ToUpper())" { Paragraph "The following section provides a summary of the Active Directory Domain Information." BlankLine @@ -187,13 +194,13 @@ function Invoke-AsBuiltReport.Microsoft.AD { if ($InfoLevel.Domain -ge 2) { Section -Style Heading5 "Roles" { - Paragraph "The following section provides a summary of the Domain Controller Role & Features information." + Paragraph "The following section provides a summary of installed role & features on $Domain DCs." foreach ($DC in $DCs){ $DCStatus = Test-Connection -ComputerName $DC -Quiet -Count 1 if ($DCStatus -eq $false) { Write-PScriboMessage -IsWarning "Unable to connect to $DC. Removing it from the $Domain report" } - if ($DC -notin $Options.Exclude.DCs -and $DCStatus) { + if (($DC -notin $Options.Exclude.DCs) -and $DCStatus) { Get-AbrADDCRoleFeature -DC $DC } } @@ -205,7 +212,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { Paragraph "The following section provides a summary of the Active Directory DC Diagnostic." BlankLine foreach ($DC in $DCs){ - if ($DC -notin $Options.Exclude.DCs -and (Test-Connection -ComputerName $DC -Quiet -Count 1)) { + if (($DC -notin $Options.Exclude.DCs) -and (Test-Connection -ComputerName $DC -Quiet -Count 1)) { Get-AbrADDCDiag -Domain $Domain -DC $DC } } @@ -221,7 +228,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { Section -Style Heading5 "Infrastructure Services Status" { Paragraph "The following section provides a summary of the Domain Controller Infrastructure services status." foreach ($DC in $DCs){ - if ($DC -notin $Options.Exclude.DCs -and (Test-Connection -ComputerName $DC -Quiet -Count 1)) { + if (($DC -notin $Options.Exclude.DCs) -and (Test-Connection -ComputerName $DC -Quiet -Count 1)) { Get-AbrADInfrastructureService -DC $DC } } @@ -238,6 +245,8 @@ function Invoke-AsBuiltReport.Microsoft.AD { Get-AbrADOU -Domain $Domain } } + } else { + Write-PScriboMessage "$($Domain) disabled in Exclude.Domain variable" } } catch { @@ -264,7 +273,14 @@ function Invoke-AsBuiltReport.Microsoft.AD { foreach ($Domain in $OrderedDomains.split(" ")) { if ($Domain) { try { - if (($Domain -notin $Options.Exclude.Domains) -and (Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop})) { + # Define Filter option for Domain variable + if ($Options.Include.Domains) { + $DomainFilterOption = $Domain -in $Options.Include.Domains + + } else { + $DomainFilterOption = $Domain -notin $Options.Exclude.Domains + } + if (( $DomainFilterOption ) -and (Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop})) { Section -Style Heading3 "$($Domain.ToString().ToUpper())" { Paragraph "The following section provides a configuration summary of the DNS service." BlankLine @@ -275,8 +291,13 @@ function Invoke-AsBuiltReport.Microsoft.AD { $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication Get-AbrADDNSZone -Domain $Domain -DC $DC } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } } } + } else { + Write-PScriboMessage "$($Domain) disabled in Exclude.Domain variable" } } catch {