From 34955d8e08cd36b85ef11e45a79e5d66a0128f41 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 28 Jun 2023 17:21:40 -0400 Subject: [PATCH 01/22] Improve Organizational Unit table #118 --- Src/Private/Get-AbrADOU.ps1 | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Src/Private/Get-AbrADOU.ps1 b/Src/Private/Get-AbrADOU.ps1 index 9342b06..eaf3d5e 100644 --- a/Src/Private/Get-AbrADOU.ps1 +++ b/Src/Private/Get-AbrADOU.ps1 @@ -31,7 +31,7 @@ function Get-AbrADOU { try { $DC = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADDomainController -Discover -Domain $using:Domain | Select-Object -ExpandProperty HostName} Write-PscriboMessage "Discovered Active Directory Organizational Unit information on DC $DC. (Organizational Unit)" - $OUs = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADOrganizationalUnit -Server $using:DC -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName -Filter *} + $OUs = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADOrganizationalUnit -Server $using:DC -Properties * -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName -Filter *} if ($OUs) { Section -Style Heading4 "Organizational Units" { Paragraph "The following section provides a summary of Active Directory Organizational Unit information." @@ -53,9 +53,9 @@ function Get-AbrADOU { } } $inObj = [ordered] @{ - 'Name' = $OU.Name - 'Path' = ConvertTo-ADCanonicalName -DN $OU.DistinguishedName -Domain $Domain -DC $DC + 'Name' = ((ConvertTo-ADCanonicalName -DN $OU.DistinguishedName -Domain $Domain -DC $DC).split('/') | Select-Object -Skip 1) -join "/" 'Linked GPO' = ConvertTo-EmptyToFiller ($GPOArray -join ", ") + 'Protected' = ConvertTo-TextYN $OU.ProtectedFromAccidentalDeletion } $OutObj += [pscustomobject]$inobj } @@ -64,15 +64,27 @@ function Get-AbrADOU { } } + if ($HealthCheck.Domain.BestPractice) { + $OutObj | Where-Object { $_.'Protected' -eq 'No' } | Set-Style -Style Warning -Property 'Protected' + } + $TableParams = @{ Name = "Organizational Unit - $($Domain.ToString().ToUpper())" List = $false - ColumnWidths = 25, 40, 35 + ColumnWidths = 45, 45, 10 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - $OutObj | Sort-Object -Property 'Path' | Table @TableParams + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + if ($HealthCheck.Domain.BestPractice -and ($OutObj | Where-Object { $_.'Protected' -eq 'No' })) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "If the Organizational Units 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. All OUs in this domain should be protected from accidental deletion" + } + } if ($HealthCheck.Domain.GPO) { try { $OutObj = @() From b3f97dd9e0b58b38dfff38c000bda12489aa9da4 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 28 Jun 2023 17:21:58 -0400 Subject: [PATCH 02/22] Add RID percent utilization health check #117 --- Src/Private/Get-AbrADDomain.ps1 | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Src/Private/Get-AbrADDomain.ps1 b/Src/Private/Get-AbrADDomain.ps1 index 11b0c6c..fc649ec 100644 --- a/Src/Private/Get-AbrADDomain.ps1 +++ b/Src/Private/Get-AbrADDomain.ps1 @@ -59,10 +59,16 @@ 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/Available' = "$($RIDsIssued) / $($RIDsRemaining)" + 'RID Issued/Available' = try {"$($RIDsIssued) / $($RIDsRemaining) ($([math]::Truncate($CompleteSIDS / $RIDsRemaining))% Issued)"} catch {"$($RIDsIssued)/$($RIDsRemaining)"} } $OutObj += [pscustomobject]$inobj + if ($HealthCheck.Domain.BestPractice) { + if ([math]::Truncate($CompleteSIDS / $RIDsRemaining) -gt 80) { + $OutObj | Set-Style -Style Warning -Property 'RID Issued/Available' + } + } + $TableParams = @{ Name = "Domain Summary - $($Domain.ToString().ToUpper())" List = $true @@ -72,6 +78,19 @@ function Get-AbrADDomain { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Table @TableParams + if ($HealthCheck.Domain.BestPractice -and ([math]::Truncate($CompleteSIDS / $RIDsRemaining) -gt 80)) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "The RID Issued is greater than 80%, a thorough evaluation of their utilization is recommended to prevent RIDs from being exhausted." + } + BlankLine + Paragraph { + Text "Reference:" -Bold + Text "https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/managing-rid-pool-depletion/ba-p/399736" + } + } } } catch { From 9915024a57ea786db1327dbbdd4edf34890a5abc Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 28 Jun 2023 17:23:50 -0400 Subject: [PATCH 03/22] Health Check : No more than 2 DNS servers Forwarders #116 Improve Domain Controller DNS IP Configuration Health Check #113 --- Src/Private/Get-AbrADDNSInfrastructure.ps1 | 125 +++++++++++++++++---- 1 file changed, 106 insertions(+), 19 deletions(-) diff --git a/Src/Private/Get-AbrADDNSInfrastructure.ps1 b/Src/Private/Get-AbrADDNSInfrastructure.ps1 index b0162c5..1a458b3 100644 --- a/Src/Private/Get-AbrADDNSInfrastructure.ps1 +++ b/Src/Private/Get-AbrADDNSInfrastructure.ps1 @@ -36,7 +36,7 @@ function Get-AbrADDNSInfrastructure { BlankLine $OutObj = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting Domain Name System Infrastructure information from '$($DC)'." try { $DNSSetting = Get-DnsServerSetting -CimSession $TempCIMSession -ComputerName $DC @@ -74,7 +74,7 @@ function Get-AbrADDNSInfrastructure { Paragraph "The following section provides Directory Partition information." BlankLine foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Section -ExcludeFromTOC -Style NOTOCHeading6 $($DC.ToString().ToUpper().Split(".")[0]) { $OutObj = @() Write-PscriboMessage "Collecting Directory Partition information from $($DC)." @@ -132,7 +132,7 @@ function Get-AbrADDNSInfrastructure { Section -Style Heading5 "Response Rate Limiting (RRL)" { $OutObj = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting Response Rate Limiting (RRL) information from $($DC)." try { $DNSSetting = Get-DnsServerResponseRateLimiting -CimSession $TempCIMSession -ComputerName $DC @@ -177,7 +177,7 @@ function Get-AbrADDNSInfrastructure { Section -Style Heading5 "Scavenging Options" { $OutObj = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting Scavenging Options information from $($DC)." try { $DNSSetting = Get-DnsServerScavenging -CimSession $TempCIMSession -ComputerName $DC @@ -239,14 +239,14 @@ function Get-AbrADDNSInfrastructure { Section -Style Heading5 "Forwarder Options" { $OutObj = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting Forwarder Options information from $($DC)." try { $DNSSetting = Get-DnsServerForwarder -CimSession $TempCIMSession -ComputerName $DC $Recursion = Get-DnsServerRecursion -CimSession $TempCIMSession -ComputerName $DC | Select-Object -ExpandProperty Enable $inObj = [ordered] @{ 'DC Name' = $($DC.ToString().ToUpper().Split(".")[0]) - 'IP Address' = $DNSSetting.IPAddress + 'IP Address' = $DNSSetting.IPAddress.IPAddressToString 'Timeout' = ("$($DNSSetting.Timeout)/s") 'Use Root Hint' = ConvertTo-EmptyToFiller (ConvertTo-TextYN $DNSSetting.UseRootHint) 'Use Recursion' = ConvertTo-EmptyToFiller (ConvertTo-TextYN $Recursion) @@ -258,6 +258,12 @@ function Get-AbrADDNSInfrastructure { } } } + + if ($HealthCheck.DNS.BestPractice) { + $OutObj | Where-Object { $_.'IP Address'.Count -gt 2 } | Set-Style -Style Warning -Property 'IP Address' + $OutObj | Where-Object { $_.'IP Address'.Count -lt 2 } | Set-Style -Style Warning -Property 'IP Address' + } + $TableParams = @{ Name = "Forwarders - $($Domain.ToString().ToUpper())" List = $false @@ -267,6 +273,28 @@ function Get-AbrADDNSInfrastructure { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams + if ($HealthCheck.DNS.BestPractice -and (($OutObj | Where-Object { $_.'IP Address' -gt 2 }) -or ($OutObj | Where-Object { $_.'IP Address'.Count -lt 2 }))) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'IP Address' -gt 2 }) { + + Paragraph { + Text "Best Practices:" -Bold + Text "Configure the servers to use no more than two external DNS servers as Forwarders." + } + Paragraph { + Text "Reference:" -Bold + Text "https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/forwarders-resolution-timeouts" + } + } + if ($OutObj | Where-Object { $_.'IP Address'.Count -lt 2 }) { + + Paragraph { + Text "Best Practices:" -Bold + Text "For redundancy reason, more than one forwarding server should be configured" + } + } + } } } catch { @@ -278,40 +306,99 @@ function Get-AbrADDNSInfrastructure { if ($InfoLevel.DNS -ge 2) { try { Section -Style Heading5 "Root Hints" { - Paragraph "The following section provides Root Hints information." + Paragraph "The following section provides Root Hints information from domain $($Domain)." + BlankLine foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Section -ExcludeFromTOC -Style NOTOCHeading6 $($DC.ToString().ToUpper().Split(".")[0]) { $OutObj = @() Write-PscriboMessage "Collecting Root Hint information from $($DC)." try { - $DNSSetting = Get-DnsServerRootHint -CimSession $TempCIMSession -ComputerName $DC | Select-Object @{Name="Name"; E={$_.NameServer.RecordData.Nameserver}},@{Name="IPAddress"; E={$_.IPAddress.RecordData.IPv6Address.IPAddressToString,$_.IPAddress.RecordData.IPv4Address.IPAddressToString} } - foreach ($Hints in $DNSSetting) { - try { + $DNSSetting = Get-DnsServerRootHint -CimSession $TempCIMSession -ComputerName $DC -ErrorAction SilentlyContinue | Select-Object @{Name="Name"; E={$_.NameServer.RecordData.Nameserver}},@{ Name="IPv4Address"; E={$_.IPAddress.RecordData.IPv4Address.IPAddressToString}},@{ Name="IPv6Address"; E={$_.IPAddress.RecordData.IPv6Address.IPAddressToString}} + if ($DNSSetting) { + foreach ($Hints in $DNSSetting) { + try { + $inObj = [ordered] @{ + 'Name' = $Hints.Name + 'IPv4 Address' = Switch ([string]::IsNullOrEmpty($Hints.IPv4Address)) { + $true {'--'} + $false {$Hints.IPv4Address -split " "} + default {'Unknown'} + } + 'IPv6 Address' = Switch ([string]::IsNullOrEmpty($Hints.IPv6Address)) { + $true {'--'} + $false {$Hints.IPv6Address -split " "} + default {'Unknown'} + } + } + $OutObj += [pscustomobject]$inobj + } + catch { + Write-PscriboMessage -IsWarning $_.Exception.Message + } + } + } else { + $RootServers = @( + "a.root-servers.net", + "b.root-servers.net", + "c.root-servers.net", + "d.root-servers.net", + "e.root-servers.net", + "f.root-servers.net", + "g.root-servers.net", + "h.root-servers.net", + "i.root-servers.net", + "j.root-servers.net", + "k.root-servers.net", + "l.root-servers.net", + "m.root-servers.net" + ) + foreach ($server in $RootServers) { $inObj = [ordered] @{ - 'Name' = $Hints.Name - 'IP Address' = (($Hints.IPAddress).Where({ $_ -ne $Null })) -join ", " + 'Name' = $server + 'IPv4 Address' = "--" + 'IPV6 Address' = "--" } $OutObj += [pscustomobject]$inobj } - catch { - Write-PscriboMessage -IsWarning $_.Exception.Message - } + } } catch { Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Root Hints Item)" } + if ($HealthCheck.DNS.BestPractice) { + $OutObj | Where-Object { $_.'IPv4 Address' -eq '--' -and $_.'IPv6 Address' -eq '--' } | Set-Style -Style Warning -Property 'IPv4 Address','IPv6 Address' + $OutObj | Where-Object { $_.'IPv4 Address'.Count -gt 1 } | Set-Style -Style Warning -Property 'IPv4 Address' + $OutObj | Where-Object { $_.'IPv6 Address'.Count -gt 1 } | Set-Style -Style Warning -Property 'IPv6 Address' + } + $TableParams = @{ - Name = "Root Hints - $($Domain.ToString().ToUpper())" + Name = "Root Hints - $($DC.ToString().ToUpper().Split(".")[0])" List = $false - ColumnWidths = 40, 60 + ColumnWidths = 40, 30, 30 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams + if ($HealthCheck.DNS.BestPractice -and (($OutObj | Where-Object { $_.'IPv4 Address' -eq '--' -and $_.'IPv6 Address' -eq '--' }) -or (($OutObj | Where-Object { $_.'IPv4 Address'.Count -gt 1 }) -or ($OutObj | Where-Object { $_.'IPv6 Address'.Count -gt 1 })))) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'IPv4 Address' -eq '--' -and $_.'IPv6 Address' -eq '--' }) { + Paragraph { + Text "Corrective Actions:" -Bold + Text "A default installation of the DNS server role should have root hints unless the server has a root zone - .(root). If the server has a root zone then delete it. If the server doesn't have a root zone and there are no root servers listed on the Root Hints tab of the DNS server properties then the server may be missing the cache.dns file in the %systemroot%\system32\dns directory, which is where the list of root servers is loaded from." + } + } + if (($OutObj | Where-Object { $_.'IPv4 Address'.Count -gt 1 }) -or ($OutObj | Where-Object { $_.'IPv6 Address'.Count -gt 1 })) { + Paragraph { + Text "Corrective Actions:" -Bold + Text "Duplicate IP Address found in the talbe of the DNS root hints servers. The DNS console does not show the duplicate Root Hint servers; you can only see them using the DNS PowerShell cmdlets. While there is a dnscmd utility to replace the Root Hints file, Using PowerShell is the best way to remediate this issue." + } + } + } } } } @@ -329,7 +416,7 @@ function Get-AbrADDNSInfrastructure { Section -Style Heading5 "Zone Scope Recursion" { $OutObj = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting Zone Scope Recursion information from $($DC)." try { $DNSSetting = Get-DnsServerRecursionScope -CimSession $TempCIMSession -ComputerName $DC From 676e35a28da21b40aad1f5ef0291157604649748 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 28 Jun 2023 20:00:31 -0400 Subject: [PATCH 04/22] Health Check: All domains should have at least two domain controllers for redundancy #120 --- Src/Private/Get-AbrADDomainController.ps1 | 71 +++++++++++++++-------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index 121754d..dc476ab 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -33,7 +33,7 @@ function Get-AbrADDomainController { $OutObj = @() Write-PscriboMessage "Discovering Active Directory Domain Controller information from $Domain." foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controllers information of $DC." $DCInfo = Invoke-Command -Session $TempPssSession {Get-ADDomainController -Identity $using:DC -Server $using:DC} @@ -53,6 +53,10 @@ function Get-AbrADDomainController { } } + if ($HealthCheck.DomainController.BestPractice) { + $OutObj.Count -eq 1 | Set-Style -Style Warning + } + $TableParams = @{ Name = "Domain Controllers - $($Domain.ToString().ToUpper())" List = $false @@ -62,6 +66,14 @@ function Get-AbrADDomainController { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams + if ($HealthCheck.DomainController.BestPractice -and ($OutObj.Count -eq 1)) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "All domains should have at least two functioning domain controllers for redundancy.In the event of a failure on the domain's only domain controller, users will not be able to log in to the domain or access domain resources." + } + } } catch { Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Table)" @@ -75,7 +87,7 @@ function Get-AbrADDomainController { Write-PscriboMessage "Discovering Active Directory Domain Controller information in $Domain." $DCHWInfo = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controller Hardware information for $DC." $CimSession = New-CimSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication @@ -90,9 +102,7 @@ function Get-AbrADDomainController { $inObj = [ordered] @{ 'Name' = $HW.CsName 'Windows Product Name' = $HW.WindowsProductName - 'Windows Current Version' = $HW.WindowsCurrentVersion 'Windows Build Number' = $HW.OsVersion - 'Windows Install Type' = $HW.WindowsInstallationType 'AD Domain' = $HW.CsDomain 'Windows Installation Date' = $HW.OsInstallDate 'Time Zone' = $HW.TimeZone @@ -100,10 +110,6 @@ function Get-AbrADDomainController { 'Partial Product Key' = $License.PartialProductKey 'Manufacturer' = $HW.CsManufacturer 'Model' = $HW.CsModel - 'Serial Number' = $HostBIOS.SerialNumber - 'Bios Type' = $HW.BiosFirmwareType - 'BIOS Version' = $HostBIOS.Version - 'Processor Manufacturer' = $HWCPU[0].Manufacturer 'Processor Model' = $HWCPU[0].Name 'Number of Processors' = ($HWCPU | Measure-Object).Count 'Number of CPU Cores' = $HWCPU[0].NumberOfCores @@ -154,9 +160,7 @@ function Get-AbrADDomainController { } } 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' - } + $DCHWInfo | Where-Object {[int]([regex]::Matches($_.'Physical Memory', "\d+(\.*\d+)").value) -lt 8} | Set-Style -Style Warning -Property 'Physical Memory' } $TableParams = @{ Name = "Hardware Inventory - $($Domain.ToString().ToUpper())" @@ -169,7 +173,7 @@ function Get-AbrADDomainController { } $DCHWInfo | Table @TableParams if ($HealthCheck.DomainController.Diagnostic) { - if ([int]([regex]::Matches($DCHWInfo.'Physical Memory', "\d+(\.*\d+)").value) -lt 8) { + if ($DCHWInfo | Where-Object {[int]([regex]::Matches($_.'Physical Memory', "\d+(\.*\d+)").value) -lt 8}) { Paragraph "Health Check:" -Bold -Underline BlankLine Paragraph { @@ -191,10 +195,11 @@ function Get-AbrADDomainController { Section -Style Heading5 "DNS IP Configuration" { $OutObj = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting DNS IP Configuration information from $($DC)." $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication try { + $DCIPAddress = Invoke-Command -Session $DCPssSession {[System.Net.Dns]::GetHostAddresses($using:DC).IPAddressToString} $DNSSettings = Invoke-Command -Session $DCPssSession { Get-NetAdapter | Get-DnsClientServerAddress -AddressFamily IPv4 } foreach ($DNSSetting in $DNSSettings) { try { @@ -202,7 +207,7 @@ function Get-AbrADDomainController { 'DC Name' = $DC.ToString().ToUpper().Split(".")[0] 'Interface' = $DNSSetting.InterfaceAlias 'Prefered DNS' = ConvertTo-EmptyToFiller $DNSSetting.ServerAddresses[0] - 'Alternate NS' = ConvertTo-EmptyToFiller $DNSSetting.ServerAddresses[1] + 'Alternate DNS' = ConvertTo-EmptyToFiller $DNSSetting.ServerAddresses[1] 'DNS 3' = ConvertTo-EmptyToFiller $DNSSetting.ServerAddresses[2] 'DNS 4' = ConvertTo-EmptyToFiller $DNSSetting.ServerAddresses[3] } @@ -223,7 +228,9 @@ function Get-AbrADDomainController { } if ($HealthCheck.DomainController.BestPractice) { - $OutObj | Where-Object { $_.'DNS IP 1' -eq "127.0.0.1"} | Set-Style -Style Warning -Property 'DNS IP 1' + $OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"} | Set-Style -Style Warning -Property 'Prefered DNS' + $OutObj | Where-Object { $_.'Prefered DNS' -in $DCIPAddress } | Set-Style -Style Warning -Property 'Prefered DNS' + $OutObj | Where-Object { $_.'Alternate DNS' -eq "--"} | Set-Style -Style Warning -Property 'Alternate DNS' } $TableParams = @{ @@ -235,12 +242,28 @@ function Get-AbrADDomainController { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams - if ($HealthCheck.DomainController.BestPractice -and ($OutObj | Where-Object { $_.'DNS IP 1' -eq "127.0.0.1"})) { + if ($HealthCheck.DomainController.BestPractice -and (($OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"}) -or ($OutObj | Where-Object { $_.'Prefered DNS' -in $DCIPAddress }) -or ($OutObj | Where-Object { $_.'Alternate DNS' -eq "--"}))) { Paragraph "Health Check:" -Bold -Underline BlankLine - Paragraph { - Text "Best Practices:" -Bold - Text "DNS configuration on network adapter should include the loopback address, but not as the first entry." + if ($OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"}) { + Paragraph { + Text "Best Practices:" -Bold + Text "DNS configuration on network adapter should include the loopback address, but not as the first entry." + } + } + if ($OutObj | Where-Object { $_.'Prefered DNS' -in $DCIPAddress }) { + BlankLine + Paragraph { + Text "Best Practices:" -Bold + Text "DNS configuration on network adapter shouldn't include the Domain Controller own IP address as the first entry." + } + } + if ($OutObj | Where-Object { $_.'Alternate DNS' -eq "--"}) { + BlankLine + Paragraph { + Text "Best Practices:" -Bold + Text "For redundancy reasons, the DNS configuration on the network adapter should include an Alternate DNS address." + } } } } @@ -254,7 +277,7 @@ function Get-AbrADDomainController { $OutObj = @() Write-PscriboMessage "Discovering Active Directory Domain Controller information in $Domain." foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controller NTDS information for $DC." $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication @@ -300,7 +323,7 @@ function Get-AbrADDomainController { $OutObj = @() Write-PscriboMessage "Discovering Active Directory Domain Controller information in $Domain." foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controller Time Source information for $DC." $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication @@ -357,7 +380,7 @@ function Get-AbrADDomainController { $OutObj = @() Write-PscriboMessage "Discovering Active Directory Domain Controller SRV Records Status in $Domain." foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controller SRV Records Status for $DC." $CimSession = New-CimSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication @@ -471,7 +494,7 @@ function Get-AbrADDomainController { Write-PscriboMessage "Discovering Active Directory Domain Controller information in $Domain." $DCObj = @() $DCObj += foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { $Software = @() Write-PscriboMessage "Collecting AD Domain Controller installed software information for $DC." @@ -546,7 +569,7 @@ function Get-AbrADDomainController { try { $DCObj = @() $DCObj += foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting pending/missing patch information from Domain Controller $($DC)." try { $Software = @() From d6fc26975339c5d80f380a7a12160266ef36a210 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 29 Jun 2023 11:18:52 -0400 Subject: [PATCH 05/22] Fix DCs discovery --- Src/Private/Get-AbrADDFSHealth.ps1 | 4 ++-- Src/Private/Get-AbrADDNSInfrastructure.ps1 | 3 ++- Src/Private/Get-AbrADDomain.ps1 | 2 +- Src/Private/Get-AbrADDomainObject.ps1 | 4 ++-- Src/Private/Get-AbrADGPO.ps1 | 8 ++++---- Src/Private/Get-AbrADKerberosAudit.ps1 | 2 +- Src/Private/Get-AbrADOU.ps1 | 2 +- Src/Private/Get-AbrADSecurityAssessment.ps1 | 2 +- Src/Private/Get-AbrADSiteReplication.ps1 | 4 ++-- Src/Private/Get-AbrADTrust.ps1 | 2 +- 10 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Src/Private/Get-AbrADDFSHealth.ps1 b/Src/Private/Get-AbrADDFSHealth.ps1 index d1d7394..cd4e6d7 100644 --- a/Src/Private/Get-AbrADDFSHealth.ps1 +++ b/Src/Private/Get-AbrADDFSHealth.ps1 @@ -103,7 +103,7 @@ function Get-AbrADDFSHealth { } try { Write-PscriboMessage "Discovered AD Domain Sysvol Health information from $Domain." - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication # Code taken from ClaudioMerola (https://github.com/ClaudioMerola/ADxRay) $SYSVOLFolder = Invoke-Command -Session $DCPssSession {Get-ChildItem -path $('\\'+$using:Domain+'\SYSVOL\'+$using:Domain) -Recurse | Where-Object -FilterScript {$_.PSIsContainer -eq $false} | Group-Object -Property Extension | ForEach-Object -Process { @@ -165,7 +165,7 @@ function Get-AbrADDFSHealth { } try { Write-PscriboMessage "Discovered AD Domain Netlogon Health information from $Domain." - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication # Code taken from ClaudioMerola (https://github.com/ClaudioMerola/ADxRay) $NetlogonFolder = Invoke-Command -Session $DCPssSession {Get-ChildItem -path $('\\'+$using:Domain+'\NETLOGON\') -Recurse | Where-Object -FilterScript {$_.PSIsContainer -eq $false} | Group-Object -Property Extension | ForEach-Object -Process { diff --git a/Src/Private/Get-AbrADDNSInfrastructure.ps1 b/Src/Private/Get-AbrADDNSInfrastructure.ps1 index 1a458b3..a6a77fc 100644 --- a/Src/Private/Get-AbrADDNSInfrastructure.ps1 +++ b/Src/Private/Get-AbrADDNSInfrastructure.ps1 @@ -282,13 +282,14 @@ function Get-AbrADDNSInfrastructure { Text "Best Practices:" -Bold Text "Configure the servers to use no more than two external DNS servers as Forwarders." } + BlankLine Paragraph { Text "Reference:" -Bold Text "https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/forwarders-resolution-timeouts" } + BlankLine } if ($OutObj | Where-Object { $_.'IP Address'.Count -lt 2 }) { - Paragraph { Text "Best Practices:" -Bold Text "For redundancy reason, more than one forwarding server should be configured" diff --git a/Src/Private/Get-AbrADDomain.ps1 b/Src/Private/Get-AbrADDomain.ps1 index fc649ec..b473910 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.13 + Version: 0.7.14 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index e30e3e7..8896047 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -34,7 +34,7 @@ function Get-AbrADDomainObject { 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:DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).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 } @@ -426,7 +426,7 @@ function Get-AbrADDomainObject { Write-PscriboMessage -IsWarning $($_.Exception.Message) } try { - Section -Style Heading4 'Privileged Group Stats' { + Section -Style Heading4 'Privileged Group Summary' { $OutObj = @() if ($Domain) { Write-PscriboMessage "Collecting Privileged Group in Active Directory." diff --git a/Src/Private/Get-AbrADGPO.ps1 b/Src/Private/Get-AbrADGPO.ps1 index 9a991c9..e768299 100644 --- a/Src/Private/Get-AbrADGPO.ps1 +++ b/Src/Private/Get-AbrADGPO.ps1 @@ -174,7 +174,7 @@ function Get-AbrADGPO { } if ($InfoLevel.Domain -ge 2) { try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $DCPssSession = New-PSSession -ComputerName $DC -Name "WmiFilters" -Credential $Credential -Authentication $Options.PSDefaultAuthentication $DomainInfo = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop} $WmiFilters = Get-ADObjectSearch -DN "CN=SOM,CN=WMIPolicy,CN=System,$($DomainInfo.DistinguishedName)" -Filter { objectClass -eq "msWMI-Som" } -SelectPrty '*' -Session $DCPssSession | Sort-Object @@ -249,7 +249,7 @@ function Get-AbrADGPO { BlankLine Paragraph { Text "Best Practices:" -Bold - Text "Ensure Central Store is deployed to centralized GPO repository." + Text "The group policy central store is a central location to store all the group policy template files. This eliminates the need for admins to load and open group policy template files on systems used to manage group policy. Ensure Central Store is deployed to centralized GPO repository." } } } @@ -473,7 +473,7 @@ function Get-AbrADGPO { try { $OutObj = @() Write-PscriboMessage "Discovered Active Directory Group Policy Objects information on $Domain. (Group Policy Objects)" - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} Write-PscriboMessage "Discovered Active Directory Domain Controller $DC in $Domain. (Group Policy Objects)" $OUs = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADOrganizationalUnit -Server $using:DC -Filter * | Select-Object -Property DistinguishedName} if ($OUs) { @@ -530,7 +530,7 @@ function Get-AbrADGPO { # Code taken from Jeremy Saunders # https://github.com/jeremyts/ActiveDirectoryDomainServices/blob/master/Audit/FindOrphanedGPOs.ps1 try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication $DomainInfo = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain -ErrorAction Stop} $GPOPoliciesSYSVOLUNC = "\\$Domain\SYSVOL\$Domain\Policies" diff --git a/Src/Private/Get-AbrADKerberosAudit.ps1 b/Src/Private/Get-AbrADKerberosAudit.ps1 index f168934..3e905e7 100644 --- a/Src/Private/Get-AbrADKerberosAudit.ps1 +++ b/Src/Private/Get-AbrADKerberosAudit.ps1 @@ -30,7 +30,7 @@ function Get-AbrADKerberosAudit { process { if ($HealthCheck.Domain.Security) { try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $Unconstrained = Invoke-Command -Session $TempPssSession {Get-ADComputer -Filter { (TrustedForDelegation -eq $True) -AND (PrimaryGroupID -ne '516') -AND (PrimaryGroupID -ne '521') } -Server $using:DC -Searchbase (Get-ADDomain -Identity $using:Domain).distinguishedName} Write-PscriboMessage "Discovered Unconstrained Kerberos Delegation information from $Domain." if ($Unconstrained) { diff --git a/Src/Private/Get-AbrADOU.ps1 b/Src/Private/Get-AbrADOU.ps1 index eaf3d5e..dc79b86 100644 --- a/Src/Private/Get-AbrADOU.ps1 +++ b/Src/Private/Get-AbrADOU.ps1 @@ -88,7 +88,7 @@ function Get-AbrADOU { if ($HealthCheck.Domain.GPO) { try { $OutObj = @() - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} Write-PscriboMessage "Discovered Active Directory Domain Controller $DC in $Domain. (Group Policy Objects)" $OUs = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADOrganizationalUnit -Server $using:DC -Filter * | Select-Object -Property DistinguishedName} if ($OUs) { diff --git a/Src/Private/Get-AbrADSecurityAssessment.ps1 b/Src/Private/Get-AbrADSecurityAssessment.ps1 index 8ee69a3..e096255 100644 --- a/Src/Private/Get-AbrADSecurityAssessment.ps1 +++ b/Src/Private/Get-AbrADSecurityAssessment.ps1 @@ -265,7 +265,7 @@ function Get-AbrADSecurityAssessment { 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} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $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) { diff --git a/Src/Private/Get-AbrADSiteReplication.ps1 b/Src/Private/Get-AbrADSiteReplication.ps1 index 4c499d1..4064da5 100644 --- a/Src/Private/Get-AbrADSiteReplication.ps1 +++ b/Src/Private/Get-AbrADSiteReplication.ps1 @@ -35,7 +35,7 @@ function Get-AbrADSiteReplication { try { $ReplInfo = @() foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { $Replication = Invoke-Command -Session $TempPssSession -ScriptBlock {Get-ADReplicationConnection -Server $using:DC -Properties *} if ($Replication) { try { @@ -123,7 +123,7 @@ function Get-AbrADSiteReplication { try { if ($HealthCheck.Site.Replication) { Write-PscriboMessage "Discovering Active Directory Replication Status on $Domain. (Replication Status)" - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication Write-PscriboMessage "Discovered Active Directory Replication Status on $Domain. (Replication Status)" $RepStatus = Invoke-Command -Session $DCPssSession -ScriptBlock {repadmin /showrepl /repsto /csv | ConvertFrom-Csv} diff --git a/Src/Private/Get-AbrADTrust.ps1 b/Src/Private/Get-AbrADTrust.ps1 index 3611912..c365a25 100644 --- a/Src/Private/Get-AbrADTrust.ps1 +++ b/Src/Private/Get-AbrADTrust.ps1 @@ -31,7 +31,7 @@ function Get-AbrADTrust { try { if ($Domain) { try { - $DC = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Select-Object -First 1} + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} $Trusts = Invoke-Command -Session $TempPssSession {Get-ADTrust -Filter * -Server $using:DC} if ($Trusts) { Section -Style Heading4 'Domain and Trusts' { From 47e8b6b2ea97b4e29ede69d73a823177a0f83c8e Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 29 Jun 2023 11:19:44 -0400 Subject: [PATCH 06/22] Health Check: All domains should have at least two domain controllers for redundancy #120 --- Src/Private/Get-AbrADDomainController.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index dc476ab..0bf9bb2 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -54,7 +54,9 @@ function Get-AbrADDomainController { } if ($HealthCheck.DomainController.BestPractice) { - $OutObj.Count -eq 1 | Set-Style -Style Warning + if ($OutObj.Count -eq 1) { + $OutObj | Set-Style -Style Warning + } } $TableParams = @{ @@ -71,7 +73,7 @@ function Get-AbrADDomainController { BlankLine Paragraph { Text "Best Practice:" -Bold - Text "All domains should have at least two functioning domain controllers for redundancy.In the event of a failure on the domain's only domain controller, users will not be able to log in to the domain or access domain resources." + Text "All domains should have at least two functioning domain controllers for redundancy. In the event of a failure on the domain's only domain controller, users will not be able to log in to the domain or access domain resources." } } } From fdb89ad2cf1e021e8bbbd985616092b66deb8a59 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 29 Jun 2023 11:20:04 -0400 Subject: [PATCH 07/22] AD DS: The infrastructure master for this domain should be held by a domain controller that is not a global catalog server #119 --- Src/Private/Get-AbrADFSMO.ps1 | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Src/Private/Get-AbrADFSMO.ps1 b/Src/Private/Get-AbrADFSMO.ps1 index d72238b..04756a6 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.13 + Version: 0.7.14 Author: Jonathan Colon Twitter: @jcolonfzenpr Github: rebelinux @@ -32,7 +32,10 @@ function Get-AbrADFSMO { $DomainData = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain | Select-Object InfrastructureMaster, RIDMaster, PDCEmulator} $ForestData = Invoke-Command -Session $TempPssSession {Get-ADForest $using:Domain | Select-Object DomainNamingMaster, SchemaMaster} if ($DomainData -and $ForestData) { + $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication Section -Style Heading4 'FSMO Roles' { + $IsInfraMasterGC = (Invoke-Command -Session $DCPssSession {Get-ADDomainController -Identity ($using:DomainData).InfrastructureMaster}).IsGlobalCatalog $OutObj = @() try { Write-PscriboMessage "Discovered Active Directory FSMO information of domain $Domain." @@ -49,6 +52,12 @@ function Get-AbrADFSMO { Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Flexible Single Master Operations)" } + if ($HealthCheck.Domain.BestPractice) { + if ($IsInfraMasterGC) { + $OutObj | Set-Style -Style Warning -Property 'Infrastructure Master' + } + } + $TableParams = @{ Name = "FSMO Roles - $($Domain)" List = $true @@ -58,6 +67,22 @@ function Get-AbrADFSMO { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Table @TableParams + if ($HealthCheck.DomainController.BestPractice -and ($IsInfraMasterGC)) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "The infrastructure master role in the domain $($Domain.ToString().ToUpper()) should be held by a domain controller that is not a global catalog server. This issue does not affect forests that have a single domain." + } + BlankLine + Paragraph { + Text "Reference:" -Bold + Text "http://go.microsoft.com/fwlink/?LinkId=168841" + } + } + } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession } } } From 04a6bd22a8d4d201771c6b626831dff5b458f439 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 15 Jul 2023 01:11:07 -0400 Subject: [PATCH 08/22] Improve GPO Table(s) #128 --- Src/Private/Get-AbrADGPO.ps1 | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Src/Private/Get-AbrADGPO.ps1 b/Src/Private/Get-AbrADGPO.ps1 index e768299..ec80f7f 100644 --- a/Src/Private/Get-AbrADGPO.ps1 +++ b/Src/Private/Get-AbrADGPO.ps1 @@ -40,6 +40,7 @@ function Get-AbrADGPO { try { foreach ($GPO in $GPOs) { try { + [xml]$Links = Invoke-Command -Session $TempPssSession -ScriptBlock {$using:GPO | Get-GPOReport -ReportType XML} Write-PscriboMessage "Collecting Active Directory Group Policy Objects '$($GPO.DisplayName)'." $inObj = [ordered] @{ 'GPO Name' = $GPO.DisplayName @@ -52,6 +53,7 @@ function Get-AbrADGPO { } else {'No Security Filtering'} } + 'Links Count' = $Links.GPO.LinksTo.SOMPath.Count } $OutObj += [pscustomobject]$inobj } @@ -63,33 +65,42 @@ function Get-AbrADGPO { if ($HealthCheck.Domain.GPO) { $OutObj | Where-Object { $_.'GPO Status' -like 'All Settings Disabled'} | Set-Style -Style Warning -Property 'GPO Status' $OutObj | Where-Object { $_.'Security Filtering' -like 'No Security Filtering'} | Set-Style -Style Warning -Property 'Security Filtering' + $OutObj | Where-Object { $_.'Links Count' -eq 0 } | Set-Style -Style Warning -Property 'Links Count' } $TableParams = @{ Name = "GPO - $($Domain.ToString().ToUpper())" List = $false - ColumnWidths = 45, 25, 30 + ColumnWidths = 40, 25, 25, 10 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'GPO Name' | Table @TableParams - if ($HealthCheck.Domain.GPO -and (($OutObj | Where-Object { $_.'GPO Status' -like 'All Settings Disabled'}) -or ($OutObj | Where-Object { $_.'Security Filtering' -like 'No Security Filtering'}))) { + if ($HealthCheck.Domain.GPO -and (($OutObj | Where-Object { $_.'GPO Status' -like 'All Settings Disabled'}) -or ($OutObj | Where-Object { $_.'Security Filtering' -like 'No Security Filtering'}) -or ($OutObj | Where-Object { $_.'Links Count' -eq 0 }))) { Paragraph "Health Check:" -Bold -Underline BlankLine if (($OutObj | Where-Object { $_.'GPO Status' -like 'All Settings Disabled'})) { Paragraph { Text "Best Practices:" -Bold - Text "Ensure 'All Settings Disabled' GPO are removed from Active Directory." + Text "Ensure 'All Settings Disabled' GPOs are removed from Active Directory." } BlankLine } if (($OutObj | Where-Object { $_.'Security Filtering' -like 'No Security Filtering'})) { Paragraph { Text "Corrective Actions:" -Bold - Text "Determine which 'No Security Filtering' Group Policies should be deleted and delete them." + Text "Determine which 'No Security Filtering' GPOs should be deleted and delete them." } + BlankLine + } + if ($OutObj | Where-Object { $_.'Links Count' -eq '0' }) { + Paragraph { + Text "Corrective Actions:" -Bold + Text "Ensure unused or unlinked GPOs are removed from Active Directory." + } + BlankLine } } } @@ -102,6 +113,7 @@ function Get-AbrADGPO { foreach ($GPO in $GPOs) { Section -ExcludeFromTOC -Style NOTOCHeading6 "$($GPO.DisplayName)" { try { + [xml]$Links = Invoke-Command -Session $TempPssSession -ScriptBlock {$using:GPO | Get-GPOReport -ReportType XML} Write-PscriboMessage "Collecting Active Directory Group Policy Objects '$($GPO.DisplayName)'. (Group Policy Objects)" $inObj = [ordered] @{ 'GPO Status' = ($GPO.GpoStatus -creplace '([A-Z\W_]|\d+)(? Date: Sat, 15 Jul 2023 01:19:48 -0400 Subject: [PATCH 09/22] Improve GPO Table(s) #128 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b6ecd..7a2d618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # :arrows_clockwise: Microsoft AD As Built Report Changelog +## [0.7.14] - 2023-07-22 + +### Added + +- + +### Changed + +- + +### Fixed + +- Fix [#128](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/128) + ## [0.7.13] - 2023-06-22 ### Added From b54d57382025ef78ce22ceba355667ef1e2c9412 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 15 Jul 2023 16:55:59 -0400 Subject: [PATCH 10/22] Add gMSA identities health check #124 --- CHANGELOG.md | 1 + Src/Private/Get-AbrADDomainObject.ps1 | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2d618..d9b93f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Fixed - Fix [#128](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/128) +- Fix [#124](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/124) ## [0.7.13] - 2023-06-22 diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index 8896047..4febde0 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -863,10 +863,10 @@ function Get-AbrADDomainObject { 'Created' = $Account.Created 'Enabled' = ConvertTo-TextYN $Account.Enabled 'DNS Host Name' = $Account.DNSHostName - 'Host Computers' = (ConvertTo-ADObjectName -DN $Account.HostComputers -Session $TempPssSession -DC $DC) -join ", " - 'Retrieve Managed Password' = (ConvertTo-ADObjectName $Account.PrincipalsAllowedToRetrieveManagedPassword -Session $TempPssSession -DC $DC) -join ", " + 'Host Computers' = ConvertTo-EmptyToFiller ((ConvertTo-ADObjectName -DN $Account.HostComputers -Session $TempPssSession -DC $DC) -join ", ") + 'Retrieve Managed Password' = ConvertTo-EmptyToFiller ((ConvertTo-ADObjectName $Account.PrincipalsAllowedToRetrieveManagedPassword -Session $TempPssSession -DC $DC) -join ", ") 'Primary Group' = (ConvertTo-ADObjectName $Account.PrimaryGroup -Session $TempPssSession -DC $DC) -join ", " - 'Last Logon Date' = $Account.LastLogonDate + 'Last Logon Date' = ConvertTo-EmptyToFiller $Account.LastLogonDate 'Locked Out' = ConvertTo-TextYN $Account.LockedOut 'Logon Count' = $Account.logonCount 'Password Expired' = ConvertTo-TextYN $Account.PasswordExpired @@ -875,7 +875,14 @@ function Get-AbrADDomainObject { $GMSAInfo += [pscustomobject]$inobj if ($HealthCheck.Domain.GMSA) { - $GMSAInfo | Where-Object { $_.'Enabled' -notlike 'Yes'} | Set-Style -Style Warning -Property 'Enabled' + $GMSAInfo | Where-Object { $_.'Enabled' -ne 'Yes' } | Set-Style -Style Warning -Property 'Enabled' + $GMSAInfo | Where-Object {$_.'Password Last Set' -lt (Get-Date).adddays(-60)} | Set-Style -Style Warning -Property 'Password Last Set' + $GMSAInfo | Where-Object {$_.'Last Logon Date' -lt (Get-Date).adddays(-60) -or $_.'Last Logon Date' -eq '--'} | Set-Style -Style Warning -Property 'Last Logon Date' + $GMSAInfo | Where-Object { $_.'Locked Out' -eq 'Yes'} | Set-Style -Style Warning -Property 'Locked Out' + $GMSAInfo | Where-Object { $_.'Logon Count' -eq 0 } | Set-Style -Style Warning -Property 'Logon Count' + $GMSAInfo | Where-Object { $_.'Password Expired' -eq 'Yes' } | Set-Style -Style Warning -Property 'Password Expired' + $GMSAInfo | Where-Object { $_.'Host Computers' -eq '--' } | Set-Style -Style Warning -Property 'Host Computers' + $GMSAInfo | Where-Object { $_.'Retrieve Managed Password' -eq '--' } | Set-Style -Style Warning -Property 'Retrieve Managed Password' } } catch { @@ -901,8 +908,8 @@ function Get-AbrADDomainObject { $TableParams = @{ Name = "gMSA - $($Domain.ToString().ToUpper())" List = $false - Columns = 'Name', 'SamAccountName', 'DNS Host Name', 'Host Computers', 'Retrieve Managed Password', 'Primary Group', 'Enabled' - ColumnWidths = 16, 14, 16, 14, 14, 14, 12 + Columns = 'Name', 'Logon Count', 'Locked Out', 'Last Logon Date', 'Password Last Set', 'Enabled' + ColumnWidths = 25, 15, 15, 15, 15, 15 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" From 73a28b4a5b69a0846f669e67c39791ffcb2be0ac Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 18 Jul 2023 14:39:25 -0400 Subject: [PATCH 11/22] Added GPO version to InfoLevel 2 --- Src/Private/Get-AbrADGPO.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Src/Private/Get-AbrADGPO.ps1 b/Src/Private/Get-AbrADGPO.ps1 index ec80f7f..9508e74 100644 --- a/Src/Private/Get-AbrADGPO.ps1 +++ b/Src/Private/Get-AbrADGPO.ps1 @@ -120,8 +120,9 @@ function Get-AbrADGPO { 'GUID' = $GPO.Id 'Created' = $GPO.CreationTime.ToString("MM/dd/yyyy") 'Modified' = $GPO.ModificationTime.ToString("MM/dd/yyyy") - 'Description' = ConvertTo-EmptyToFiller $GPO.Description 'Owner' = $GPO.Owner + 'Computer Version' = "$($Links.GPO.Computer.VersionDirectory) (AD), $($Links.GPO.Computer.VersionSysvol) (SYSVOL)" + 'User Version' = "$($Links.GPO.User.VersionDirectory) (AD), $($Links.GPO.User.VersionSysvol) (SYSVOL)" 'WMI Filter' = &{ $WMIFilter = Invoke-Command -Session $TempPssSession -ScriptBlock {((Get-Gpo -DomainName $using:Domain -Name $using:GPO.DisplayName).WMifilter.Name)} if ($WMIFilter) { @@ -141,6 +142,7 @@ function Get-AbrADGPO { 'False' {$Links.GPO.LinksTo.SOMPath} default {'Unknown'} } + 'Description' = ConvertTo-EmptyToFiller $GPO.Description } $OutObj = [pscustomobject]$inobj From 373446fc4a19436b2a44fd6d74ab237da1468ffb Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 18 Jul 2023 22:40:03 -0400 Subject: [PATCH 12/22] Add Service Accounts Assessment healthcheck #123 --- Src/Private/Get-AbrADSecurityAssessment.ps1 | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Src/Private/Get-AbrADSecurityAssessment.ps1 b/Src/Private/Get-AbrADSecurityAssessment.ps1 index e096255..044fb71 100644 --- a/Src/Private/Get-AbrADSecurityAssessment.ps1 +++ b/Src/Private/Get-AbrADSecurityAssessment.ps1 @@ -266,7 +266,7 @@ function Get-AbrADSecurityAssessment { } try { $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} - $UserSPNs = Invoke-Command -Session $TempPssSession {Get-ADUser -ResultPageSize 1000 -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 AdminCount,PasswordLastSet,LastLogonDate,ServicePrincipalName,TrustedForDelegation,TrustedtoAuthForDelegation} Write-PscriboMessage "Discovered Service Accounts information from $Domain." if ($UserSPNs) { Section -ExcludeFromTOC -Style NOTOCHeading5 'Service Accounts Assessment' { @@ -274,6 +274,7 @@ function Get-AbrADSecurityAssessment { BlankLine $OutObj = @() Write-PscriboMessage "Collecting Service Accounts information from $($Domain)." + $AdminCount = ($UserSPNs | Where-Object {$_.AdminCount -eq 1 -and $_.SamAccountName -ne 'krbtgt' }).Name foreach ($UserSPN in $UserSPNs) { try { $inObj = [ordered] @{ @@ -296,6 +297,12 @@ function Get-AbrADSecurityAssessment { } } + if ($HealthCheck.Domain.Security) { + foreach ( $OBJ in ($OutObj | Where-Object {$_.'Username' -in $AdminCount})) { + $OBJ.Username = "** $($OBJ.Username)" + } + } + $TableParams = @{ Name = "Service Accounts Assessment - $($Domain.ToString().ToUpper())" List = $false @@ -308,9 +315,12 @@ function Get-AbrADSecurityAssessment { $OutObj | Table @TableParams Paragraph "Health Check:" -Bold -Underline BlankLine - Paragraph { - Text "Corrective Actions:" -Bold - Text "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." + if ($OutObj | Where-Object {$_.'Username' -match '\*'}) { + Paragraph { + Text "Security Best Practice:" -Bold + + Text "**Attackers are most interested in Service Accounts that are members of highly privileged groups like Domain Admins. A quick way to check for this is to enumerate all user accounts with the attribute AdminCount equal to 1. This means an attacker may just ask AD for all user accounts with a SPN and with AdminCount=1. Ensure that there are no privileged accounts that have SPNs assigned to them. " + } } } } From efe27af8d087f710a61c0a8ff01bf67eb0c01cbc Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 19 Jul 2023 21:13:42 -0400 Subject: [PATCH 13/22] Add File Shares on a Domain Controller health check #125 --- Src/Private/Get-AbrADDomainController.ps1 | 113 +++++++++++++++++++--- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index 0bf9bb2..c8c925f 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -93,7 +93,7 @@ function Get-AbrADDomainController { try { Write-PscriboMessage "Collecting AD Domain Controller Hardware information for $DC." $CimSession = New-CimSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllerHardware' $HW = Invoke-Command -Session $DCPssSession -ScriptBlock { Get-ComputerInfo } $License = Get-CimInstance -Query 'Select * from SoftwareLicensingProduct' -CimSession $CimSession | Where-Object { $_.LicenseStatus -eq 1 } $HWCPU = Get-CimInstance -Class Win32_Processor -CimSession $CimSession @@ -199,10 +199,18 @@ function Get-AbrADDomainController { foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Write-PscriboMessage "Collecting DNS IP Configuration information from $($DC)." - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DNSIPConfiguration' try { $DCIPAddress = Invoke-Command -Session $DCPssSession {[System.Net.Dns]::GetHostAddresses($using:DC).IPAddressToString} $DNSSettings = Invoke-Command -Session $DCPssSession { Get-NetAdapter | Get-DnsClientServerAddress -AddressFamily IPv4 } + $PrimaryDNSSoA = Invoke-Command -Session $DCPssSession { (Get-DnsServerResourceRecord -RRType Soa -ZoneName $using:Domain).RecordData.PrimaryServer } + $UnresolverDNS = @() + foreach ($DNSServer in $DNSSettings.ServerAddresses) { + $Unresolver = Invoke-Command -Session $DCPssSession { Resolve-DnsName -Server $using:DNSServer -Name $using:PrimaryDNSSoA -DnsOnly -ErrorAction SilentlyContinue } + if ([string]::IsNullOrEmpty($Unresolver)) { + $UnresolverDNS += $DNSServer + } + } foreach ($DNSSetting in $DNSSettings) { try { $inObj = [ordered] @{ @@ -223,16 +231,20 @@ function Get-AbrADDomainController { catch { Write-PscriboMessage -IsWarning "Domain Controller DNS IP Configuration Table Section: $($_.Exception.Message)" } + + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } } } - if ($DCPssSession) { - Remove-PSSession -Session $DCPssSession - } if ($HealthCheck.DomainController.BestPractice) { - $OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"} | Set-Style -Style Warning -Property 'Prefered DNS' - $OutObj | Where-Object { $_.'Prefered DNS' -in $DCIPAddress } | Set-Style -Style Warning -Property 'Prefered DNS' - $OutObj | Where-Object { $_.'Alternate DNS' -eq "--"} | Set-Style -Style Warning -Property 'Alternate DNS' + $OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1" -or $_.'Prefered DNS' -in $DCIPAddress } | Set-Style -Style Warning -Property 'Prefered DNS' + $OutObj | Where-Object { $_.'Alternate DNS' -eq "--" } | Set-Style -Style Warning -Property 'Alternate DNS' + $OutObj | Where-Object { $_.'Prefered DNS'-in $UnresolverDNS } | Set-Style -Style Critical -Property 'Prefered DNS' + $OutObj | Where-Object { $_.'Alternate DNS'-in $UnresolverDNS } | Set-Style -Style Critical -Property 'Alternate DNS' + $OutObj | Where-Object { $_.'DNS 3'-in $UnresolverDNS } | Set-Style -Style Critical -Property 'DNS 3' + $OutObj | Where-Object { $_.'DNS 4' -in $UnresolverDNS } | Set-Style -Style Critical -Property 'DNS 4' } $TableParams = @{ @@ -244,7 +256,7 @@ function Get-AbrADDomainController { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'DC Name' | Table @TableParams - if ($HealthCheck.DomainController.BestPractice -and (($OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"}) -or ($OutObj | Where-Object { $_.'Prefered DNS' -in $DCIPAddress }) -or ($OutObj | Where-Object { $_.'Alternate DNS' -eq "--"}))) { + if ($HealthCheck.DomainController.BestPractice -and (($OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"}) -or ($OutObj | Where-Object { $_.'Prefered DNS' -in $DCIPAddress }) -or ($OutObj | Where-Object { $_.'Alternate DNS' -eq "--"}) -or ($OutObj | Where-Object { $_.'Prefered DNS' -in $UnresolverDNS -or $_.'Alternate DNS' -in $UnresolverDNS -or $_.'DNS 3' -in $UnresolverDNS -or $_.'DNS 4' -in $UnresolverDNS }))) { Paragraph "Health Check:" -Bold -Underline BlankLine if ($OutObj | Where-Object { $_.'Prefered DNS' -eq "127.0.0.1"}) { @@ -267,12 +279,85 @@ function Get-AbrADDomainController { Text "For redundancy reasons, the DNS configuration on the network adapter should include an Alternate DNS address." } } + if ($OutObj | Where-Object { $_.'Prefered DNS' -in $UnresolverDNS -or $_.'Alternate DNS' -in $UnresolverDNS -or $_.'DNS 3' -in $UnresolverDNS -or $_.'DNS 4' -in $UnresolverDNS }) { + BlankLine + Paragraph { + Text "Corrective Actions:" -Bold + Text "Network interfaces must be configured with DNS servers that can resolve names in the forest root domain. The following DNS server did not respond to the query for the forest root domain $($Domain.ToString().toUpper()): $(($UnresolverDNS -join ", "))" + } + } } } } catch { Write-PscriboMessage -IsWarning "Domain Controller DNS IP Configuration Section: $($_.Exception.Message)" } + + try { + if ($HealthCheck.DomainController.BestPractice) { + Write-PscriboMessage "Discovering Active Directory File Shares information from $Domain." + $OutObj = foreach ($DC in $DCs) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { + try { + Write-PscriboMessage "Collecting AD Domain Controllers file shares information of $DC." + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllersFileShares' + $Shares = Invoke-Command -Session $DCPssSession { Get-SmbShare | Where-Object { $_.Description -ne 'Default share' -and $_.Description -notmatch 'Remote' -and $_.Name -ne 'NETLOGON' -and $_.Name -ne 'SYSVOL' } } + if ($Shares) { + Section -ExcludeFromTOC -Style NOTOCHeading6 $($DC.ToString().ToUpper().Split(".")[0]) { + $FSObj = @() + foreach ($Share in $Shares) { + $inObj = [ordered] @{ + 'Name' = $Share.Name + 'Path' = $Share.Path + 'Description' = ConvertTo-EmptyToFiller $Share.Description + } + $FSObj += [pscustomobject]$inobj + } + + if ($HealthCheck.DomainController.BestPractice) { + $FSObj | Set-Style -Style Warning + } + + $TableParams = @{ + Name = "File Shares - $($DC.ToString().ToUpper().Split(".")[0])" + List = $false + ColumnWidths = 34, 33, 33 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + + $FSObj | Sort-Object -Property 'Name' | Table @TableParams + } + } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (File Shares Item)" + } + } + } + + if ($OutObj) { + Section -Style Heading5 "File Shares" { + Paragraph "The following domain controllers have non-default file shares." + $OutObj + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "Only netlogon, sysvol and the default administrative shares should exist on a Domain Controller. If possible, non default file shares should be moved to another server, preferably a dedicated file server. " + } + } + } + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (File Shares Table)" + } + try { Write-PscriboMessage "Collecting AD Domain Controller NTDS information." Section -Style Heading5 'NTDS Information' { @@ -282,7 +367,7 @@ function Get-AbrADDomainController { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controller NTDS information for $DC." - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'NTDS' $NTDS = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Services\NTDS\Parameters | Select-Object -ExpandProperty 'DSA Database File'} $size = Invoke-Command -Session $DCPssSession -ScriptBlock {(Get-ItemProperty -Path $using:NTDS).Length} $LogFiles = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Services\NTDS\Parameters | Select-Object -ExpandProperty 'Database log files path'} @@ -328,7 +413,7 @@ function Get-AbrADDomainController { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { Write-PscriboMessage "Collecting AD Domain Controller Time Source information for $DC." - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'TimeSource' $NtpServer = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Services\W32Time\Parameters | Select-Object -ExpandProperty 'NtpServer'} $SourceType = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Services\W32Time\Parameters | Select-Object -ExpandProperty 'Type'} Remove-PSSession -Session $DCPssSession @@ -500,7 +585,7 @@ function Get-AbrADDomainController { try { $Software = @() Write-PscriboMessage "Collecting AD Domain Controller installed software information for $DC." - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllerInstalledSoftware' $SoftwareX64 = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {($_.Publisher -notlike "Microsoft*" -and $_.DisplayName -notlike "VMware*" -and $_.DisplayName -notlike "Microsoft*") -and ($Null -ne $_.Publisher -or $Null -ne $_.DisplayName)} | Select-Object -Property DisplayName,Publisher,InstallDate | Sort-Object -Property DisplayName} $SoftwareX86 = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {($_.Publisher -notlike "Microsoft*" -and $_.DisplayName -notlike "VMware*" -and $_.DisplayName -notlike "Microsoft*") -and ($Null -ne $_.Publisher -or $Null -ne $_.DisplayName)} | Select-Object -Property DisplayName,Publisher,InstallDate | Sort-Object -Property DisplayName} Remove-PSSession -Session $DCPssSession @@ -575,8 +660,8 @@ function Get-AbrADDomainController { Write-PscriboMessage "Collecting pending/missing patch information from Domain Controller $($DC)." try { $Software = @() - Write-PscriboMessage "Collecting AD Domain Controller installed software information for $DC." - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + Write-PscriboMessage "Collecting AD Domain Controller pending/missing patch information for $DC." + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllerPendingMissingPatch' $Updates = Invoke-Command -Session $DCPssSession -ScriptBlock {(New-Object -ComObject Microsoft.Update.Session).CreateupdateSearcher().Search("IsHidden=0 and IsInstalled=0").Updates | Select-Object Title,KBArticleIDs} Remove-PSSession -Session $DCPssSession From 0efcf1311f9e60fd1f64d2434c64fa987547a8b3 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 19 Jul 2023 21:15:07 -0400 Subject: [PATCH 14/22] Modified DC pssession so it can be identified by name --- Src/Private/Get-AbrADDCRoleFeature.ps1 | 2 +- Src/Private/Get-AbrADDFSHealth.ps1 | 4 ++-- Src/Private/Get-AbrADFSMO.ps1 | 2 +- Src/Private/Get-AbrADInfrastructureService.ps1 | 2 +- Src/Private/Get-AbrADSiteReplication.ps1 | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Src/Private/Get-AbrADDCRoleFeature.ps1 b/Src/Private/Get-AbrADDCRoleFeature.ps1 index bfc7b85..1881067 100644 --- a/Src/Private/Get-AbrADDCRoleFeature.ps1 +++ b/Src/Private/Get-AbrADDCRoleFeature.ps1 @@ -30,7 +30,7 @@ function Get-AbrADDCRoleFeature { process { Write-PscriboMessage "Collecting AD Domain Controller Role & Features information for domain $Domain" try { - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'ADDCRoleFeature' if ($DCPssSession) { Write-PscriboMessage "Discovered Active Directory DC Role & Features information of $DC." Section -ExcludeFromTOC -Style NOTOCHeading6 $($DC.ToString().ToUpper().Split(".")[0]) { diff --git a/Src/Private/Get-AbrADDFSHealth.ps1 b/Src/Private/Get-AbrADDFSHealth.ps1 index cd4e6d7..bcaf560 100644 --- a/Src/Private/Get-AbrADDFSHealth.ps1 +++ b/Src/Private/Get-AbrADDFSHealth.ps1 @@ -104,7 +104,7 @@ function Get-AbrADDFSHealth { try { Write-PscriboMessage "Discovered AD Domain Sysvol Health information from $Domain." $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainSysvolHealth' # Code taken from ClaudioMerola (https://github.com/ClaudioMerola/ADxRay) $SYSVOLFolder = Invoke-Command -Session $DCPssSession {Get-ChildItem -path $('\\'+$using:Domain+'\SYSVOL\'+$using:Domain) -Recurse | Where-Object -FilterScript {$_.PSIsContainer -eq $false} | Group-Object -Property Extension | ForEach-Object -Process { New-Object -TypeName PSObject -Property @{ @@ -166,7 +166,7 @@ function Get-AbrADDFSHealth { try { Write-PscriboMessage "Discovered AD Domain Netlogon Health information from $Domain." $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'NetlogonHealth' # Code taken from ClaudioMerola (https://github.com/ClaudioMerola/ADxRay) $NetlogonFolder = Invoke-Command -Session $DCPssSession {Get-ChildItem -path $('\\'+$using:Domain+'\NETLOGON\') -Recurse | Where-Object -FilterScript {$_.PSIsContainer -eq $false} | Group-Object -Property Extension | ForEach-Object -Process { New-Object -TypeName PSObject -Property @{ diff --git a/Src/Private/Get-AbrADFSMO.ps1 b/Src/Private/Get-AbrADFSMO.ps1 index 04756a6..ed47da4 100644 --- a/Src/Private/Get-AbrADFSMO.ps1 +++ b/Src/Private/Get-AbrADFSMO.ps1 @@ -33,7 +33,7 @@ function Get-AbrADFSMO { $ForestData = Invoke-Command -Session $TempPssSession {Get-ADForest $using:Domain | Select-Object DomainNamingMaster, SchemaMaster} if ($DomainData -and $ForestData) { $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'FSMORoles' Section -Style Heading4 'FSMO Roles' { $IsInfraMasterGC = (Invoke-Command -Session $DCPssSession {Get-ADDomainController -Identity ($using:DomainData).InfrastructureMaster}).IsGlobalCatalog $OutObj = @() diff --git a/Src/Private/Get-AbrADInfrastructureService.ps1 b/Src/Private/Get-AbrADInfrastructureService.ps1 index 708137e..e2b69d7 100644 --- a/Src/Private/Get-AbrADInfrastructureService.ps1 +++ b/Src/Private/Get-AbrADInfrastructureService.ps1 @@ -30,7 +30,7 @@ function Get-AbrADInfrastructureService { process { Write-PscriboMessage "Discovering AD Domain Controller Infrastructure Services information for $DC." try { - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllerInfrastructureServices' $Available = Invoke-Command -Session $DCPssSession -ScriptBlock {Get-Service "W32Time" | Select-Object DisplayName, Name, Status} if ($Available) { Write-PscriboMessage "Discovered Active Directory DC Infrastructure Services information of $DC." diff --git a/Src/Private/Get-AbrADSiteReplication.ps1 b/Src/Private/Get-AbrADSiteReplication.ps1 index 4064da5..010c0db 100644 --- a/Src/Private/Get-AbrADSiteReplication.ps1 +++ b/Src/Private/Get-AbrADSiteReplication.ps1 @@ -124,7 +124,7 @@ function Get-AbrADSiteReplication { if ($HealthCheck.Site.Replication) { Write-PscriboMessage "Discovering Active Directory Replication Status on $Domain. (Replication Status)" $DC = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1} - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'ActiveDirectoryReplicationStatus' Write-PscriboMessage "Discovered Active Directory Replication Status on $Domain. (Replication Status)" $RepStatus = Invoke-Command -Session $DCPssSession -ScriptBlock {repadmin /showrepl /repsto /csv | ConvertFrom-Csv} if ($RepStatus) { From 29df92c6ea8dbd263f13d3aff31c3f3dd8e1b982 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 19 Jul 2023 21:15:21 -0400 Subject: [PATCH 15/22] Improve GPO Table(s) #128 --- Src/Private/Get-AbrADGPO.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Src/Private/Get-AbrADGPO.ps1 b/Src/Private/Get-AbrADGPO.ps1 index 9508e74..4825f8f 100644 --- a/Src/Private/Get-AbrADGPO.ps1 +++ b/Src/Private/Get-AbrADGPO.ps1 @@ -40,7 +40,7 @@ function Get-AbrADGPO { try { foreach ($GPO in $GPOs) { try { - [xml]$Links = Invoke-Command -Session $TempPssSession -ScriptBlock {$using:GPO | Get-GPOReport -ReportType XML} + [xml]$Links = Invoke-Command -Session $TempPssSession -ScriptBlock {$using:GPO | Get-GPOReport -Domain $using:Domain -ReportType XML} Write-PscriboMessage "Collecting Active Directory Group Policy Objects '$($GPO.DisplayName)'." $inObj = [ordered] @{ 'GPO Name' = $GPO.DisplayName @@ -113,7 +113,7 @@ function Get-AbrADGPO { foreach ($GPO in $GPOs) { Section -ExcludeFromTOC -Style NOTOCHeading6 "$($GPO.DisplayName)" { try { - [xml]$Links = Invoke-Command -Session $TempPssSession -ScriptBlock {$using:GPO | Get-GPOReport -ReportType XML} + [xml]$Links = Invoke-Command -Session $TempPssSession -ScriptBlock {$using:GPO | Get-GPOReport -Domain $using:Domain -ReportType XML} Write-PscriboMessage "Collecting Active Directory Group Policy Objects '$($GPO.DisplayName)'. (Group Policy Objects)" $inObj = [ordered] @{ 'GPO Status' = ($GPO.GpoStatus -creplace '([A-Z\W_]|\d+)(? Date: Fri, 21 Jul 2023 22:20:12 -0400 Subject: [PATCH 16/22] Add Privileged User Groups #126 --- Src/Private/Get-AbrADDomainObject.ps1 | 585 +++++++++++++------------- 1 file changed, 297 insertions(+), 288 deletions(-) diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index 4febde0..2dbd889 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -19,29 +19,29 @@ function Get-AbrADDomainObject { [Parameter ( Position = 0, Mandatory)] - [string] - $Domain + [string] + $Domain ) begin { - Write-PscriboMessage "Discovering AD Domain Objects information on forest $Forestinfo." + Write-PScriboMessage "Discovering AD Domain Objects information on forest $Forestinfo." } process { try { Section -Style Heading4 'Domain Object Stats' { if ($Domain) { - Write-PscriboMessage "Collecting the Active Directory Object Count of domain $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).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)} + $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).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} + $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 = @() @@ -67,41 +67,40 @@ function Get-AbrADDomainObject { $addChartAreaParams = @{ Chart = $exampleChart - Name = 'exampleChartArea' + Name = 'exampleChartArea' } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'name' - YField = 'value' - Palette = 'Blue' + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'exampleChartSeries' + XField = 'name' + YField = 'value' + Palette = 'Blue' ColorPerDataPoint = $true } $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Category' - TitleAlignment = 'Center' + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' } Add-ChartLegend @addChartLegendParams $addChartTitleParams = @{ - Chart = $exampleChart + Chart = $exampleChart ChartArea = $exampleChartArea - Name = 'ComputersObject' - Text = 'Computers Count' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + Name = 'ComputersObject' + Text = 'Computers Count' + 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) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } if ($OutObj) { @@ -112,9 +111,8 @@ function Get-AbrADDomainObject { $OutObj | Table @TableParams } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { $OutObj = @() @@ -140,41 +138,40 @@ function Get-AbrADDomainObject { $addChartAreaParams = @{ Chart = $exampleChart - Name = 'exampleChartArea' + Name = 'exampleChartArea' } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'name' - YField = 'value' - Palette = 'Blue' + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'exampleChartSeries' + XField = 'name' + YField = 'value' + Palette = 'Blue' ColorPerDataPoint = $true } $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Category' - TitleAlignment = 'Center' + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' } Add-ChartLegend @addChartLegendParams $addChartTitleParams = @{ - Chart = $exampleChart + Chart = $exampleChart ChartArea = $exampleChartArea - Name = 'DomainControllerObject' - Text = 'Domain Controller Count' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + Name = 'DomainControllerObject' + Text = 'Domain Controller Count' + 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) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } if ($OutObj) { @@ -185,9 +182,8 @@ function Get-AbrADDomainObject { $OutObj | Table @TableParams } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { $OutObj = @() @@ -214,41 +210,40 @@ function Get-AbrADDomainObject { $addChartAreaParams = @{ Chart = $exampleChart - Name = 'exampleChartArea' + Name = 'exampleChartArea' } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'name' - YField = 'value' - Palette = 'Blue' + Chart = $exampleChart + ChartArea = $exampleChartArea + Name = 'exampleChartSeries' + XField = 'name' + YField = 'value' + Palette = 'Blue' ColorPerDataPoint = $true } $exampleChartSeries = $sampleData | Add-PieChartSeries @addChartSeriesParams -PassThru $addChartLegendParams = @{ - Chart = $exampleChart - Name = 'Category' - TitleAlignment = 'Center' + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' } Add-ChartLegend @addChartLegendParams $addChartTitleParams = @{ - Chart = $exampleChart + Chart = $exampleChart ChartArea = $exampleChartArea - Name = 'UsersObject' - Text = 'Users Count' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + Name = 'UsersObject' + Text = 'Users Count' + 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) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } if ($OutObj) { @@ -259,103 +254,88 @@ function Get-AbrADDomainObject { $OutObj | Table @TableParams } } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) - } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" } } } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" } try { $OutObj = @() $DaysInactive = 90 $dormanttime = ((Get-Date).AddDays(-90)).Date $passwordtime = (Get-Date).Adddays(-42) - $CannotChangePassword = $Users | Where-Object {$_.CannotChangePassword} - $PasswordNextLogon = $Users | Where-Object {$_.PasswordLastSet -eq 0 -or $_.PwdLastSet -eq 0} + $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} + $SmartcardLogonRequired = $Users | Where-Object { $_.SmartcardLogonRequired -eq $True } $SidHistory = $Users | Select-Object -ExpandProperty SIDHistory - $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 = $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 = @('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') + $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 = $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 = @('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." + Write-PScriboMessage "Collecting User Accounts in Domain." foreach ($Category in $Categories) { try { if ($Category -eq 'Total Users') { $Values = $Users - } - elseif ($Category -eq 'Cannot Change Password') { + } elseif ($Category -eq 'Cannot Change Password') { $Values = $CannotChangePassword - } - elseif ($Category -eq 'Must Change Password at Logon') { + } elseif ($Category -eq 'Must Change Password at Logon') { $Values = $PasswordNextLogon - } - elseif ($Category -eq 'Password Never Expires') { + } elseif ($Category -eq 'Password Never Expires') { $Values = $passwordNeverExpires - } - elseif ($Category -eq 'Password Age (> 42 days)') { - $Values = $PasswordLastSet | Where-Object {$_.PasswordLastSet -le $passwordtime} - } - elseif ($Category -eq 'SmartcardLogonRequired') { + } elseif ($Category -eq 'Password Age (> 42 days)') { + $Values = $PasswordLastSet | Where-Object { $_.PasswordLastSet -le $passwordtime } + } elseif ($Category -eq 'SmartcardLogonRequired') { $Values = $SmartcardLogonRequired - } - elseif ($Category -eq 'Never Logged in') { + } elseif ($Category -eq 'Never Logged in') { $Values = $NeverloggedIn - } - elseif ($Category -eq 'Dormant (> 90 days)') { + } elseif ($Category -eq 'Dormant (> 90 days)') { $Values = $Dormant - } - elseif ($Category -eq 'Password Not Required') { + } elseif ($Category -eq 'Password Not Required') { $Values = $PasswordNotRequired - } - elseif ($Category -eq 'Account Expired') { + } elseif ($Category -eq 'Account Expired') { $Values = $AccountExpired - } - elseif ($Category -eq 'Account Lockout') { + } elseif ($Category -eq 'Account Lockout') { $Values = $AccountLockout - } - elseif ($Category -eq 'SidHistory') { + } elseif ($Category -eq 'SidHistory') { $Values = $SidHistory } $inObj = [ordered] @{ 'Category' = $Category '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)} + 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)} + 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)} + 0 { '0' } + $Null { '0' } + default { [math]::Round((($Values | Measure-Object).Count / $Users.Count * 100), 2) } } } $OutObj += [pscustomobject]$inobj - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Status of User Accounts)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of User Accounts)" } } @@ -375,41 +355,40 @@ function Get-AbrADDomainObject { $addChartAreaParams = @{ Chart = $exampleChart - Name = 'exampleChartArea' + Name = 'exampleChartArea' } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'Category' - YField = 'Total' - Palette = 'Blue' + 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' + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' } Add-ChartLegend @addChartLegendParams $addChartTitleParams = @{ - Chart = $exampleChart + 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) + 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) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } } @@ -421,125 +400,168 @@ function Get-AbrADDomainObject { $OutObj | Table @TableParams } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { - Section -Style Heading4 'Privileged Group Summary' { + Section -Style Heading4 'Privileged Groups' { $OutObj = @() if ($Domain) { - Write-PscriboMessage "Collecting Privileged Group in Active Directory." + Write-PScriboMessage "Collecting Privileged Group in Active Directory." try { - $DomainSID = Invoke-Command -Session $TempPssSession {(Get-ADDomain -Identity $using:Domain).domainsid.Value} + $DomainSID = Invoke-Command -Session $TempPssSession { (Get-ADDomain -Identity $using:Domain).domainsid.Value } if ($Domain -eq $ADSystem.Name) { - $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 { - $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' + $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 { + $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) { - foreach ($GroupSID in $GroupsSID) { - try { - $Group = Invoke-Command -Session $TempPssSession {Get-ADGroup -Server $using:DC -Filter * | Where-Object {$_.SID -like $using:GroupSID}} - if ($Group) { - Write-PscriboMessage "Collecting Privileged Group $($Group.Name) with SID $($Group.SID)" - $GroupObject = Invoke-Command -Session $TempPssSession {Get-ADGroupMember -Server $using:DC -Identity ($using:Group).Name -Recursive -ErrorAction SilentlyContinue} - $inObj = [ordered] @{ - 'Group Name' = $Group.Name - 'Count' = ($GroupObject | Measure-Object).Count + if ($InfoLevel.Domain -eq 1) { + foreach ($GroupSID in $GroupsSID) { + try { + $Group = Invoke-Command -Session $TempPssSession { Get-ADGroup -Server $using:DC -Filter * | Where-Object { $_.SID -like $using:GroupSID } } + if ($Group) { + Write-PScriboMessage "Collecting Privileged Group $($Group.Name) with SID $($Group.SID)" + $GroupObject = Invoke-Command -Session $TempPssSession { Get-ADGroupMember -Server $using:DC -Identity ($using:Group).Name -Recursive -ErrorAction SilentlyContinue } + $inObj = [ordered] @{ + 'Group Name' = $Group.Name + 'Count' = ($GroupObject | Measure-Object).Count + } + $OutObj += [pscustomobject]$inobj } - $OutObj += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" } } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" - } - } - if ($HealthCheck.Domain.Security) { - $OutObj | Where-Object { $_.'Group Name' -eq 'Schema Admins' -and $_.Count -gt 1 } | Set-Style -Style Warning - } + if ($HealthCheck.Domain.Security) { + $OutObj | Where-Object { $_.'Group Name' -eq 'Schema Admins' -and $_.Count -gt 1 } | Set-Style -Style Warning + } - $TableParams = @{ - Name = "Privileged Group Stats - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 60, 40 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - $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:" -Bold -Underline - BlankLine - Paragraph { - Text "Security Best Practice:" -Bold - Text "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." + $TableParams = @{ + Name = "Privileged Groups - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 60, 40 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $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:" -Bold -Underline + BlankLine + Paragraph { + Text "Security Best Practice:" -Bold + Text "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." + } + } + } else { + foreach ($GroupSID in $GroupsSID) { + try { + $Group = Invoke-Command -Session $TempPssSession { Get-ADGroup -Server $using:DC -Filter * | Where-Object { $_.SID -like $using:GroupSID } } + if ($Group) { + Write-PScriboMessage "Collecting Privileged Group $($Group.Name) with SID $($Group.SID)" + $GroupObjects = Invoke-Command -Session $TempPssSession { Get-ADGroupMember -Server $using:DC -Identity ($using:Group).Name -Recursive -ErrorAction SilentlyContinue | ForEach-Object {Get-ADUser -Filter 'SamAccountName -eq $_.SamAccountName' -Server $using:DC -Property SamAccountName,objectClass,PasswordLastSet,passwordNeverExpires,Enabled -SearchBase (Get-ADDomain -Identity $using:Domain).distinguishedName }} + if ($GroupObjects) { + Section -ExcludeFromTOC -Style NOTOCHeading5 "$($Group.Name) ($(($GroupObjects | Measure-Object).count) Members)" { + $OutObj = @() + foreach ($GroupObject in $GroupObjects) { + $inObj = [ordered] @{ + 'Name' = $GroupObject.SamAccountName + 'Password Last Changed' = $GroupObject.PasswordLastSet + 'Password Never Expires' = ConvertTo-TextYN $GroupObject.passwordNeverExpires + 'Account Enabled' = ConvertTo-TextYN $GroupObject.Enabled + } + $OutObj += [pscustomobject]$inobj + } + + if ($HealthCheck.Domain.Security) { + $OutObj | Where-Object { $_.'Password Never Expires' -eq 'Yes' } | Set-Style -Style Warning -Property 'Password Never Expires' + $OutObj | Where-Object { $_.'Account Enabled' -eq 'No' } | Set-Style -Style Warning -Property 'Account Enabled' + $OutObj | Where-Object { $_.'Password Last Changed' -le (Get-Date).AddDays(-90) } | Set-Style -Style Warning -Property 'Password Last Changed' + } + + $TableParams = @{ + Name = "$($Group.Name) - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 50, 20, 15, 15 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + if ($HealthCheck.Domain.Security -and ($Group.Name -eq 'Schema Admins') -and ($GroupObjects | Measure-Object).count -gt 0) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Security Best Practice:" -Bold + Text "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." + } + } + } + } + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" + } } } } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory)" } } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { $OutObj = @() $DaysInactive = 90 $dormanttime = (Get-Date).Adddays(-90) $passwordtime = (Get-Date).Adddays(-30) - $Dormant = $Computers | Where-Object {[datetime]::FromFileTime($_.lastlogontimestamp) -lt $dormanttime} - $PasswordAge = $Computers | Where-Object {$_.PasswordLastSet -le $passwordtime} + $Dormant = $Computers | Where-Object { [datetime]::FromFileTime($_.lastlogontimestamp) -lt $dormanttime } + $PasswordAge = $Computers | Where-Object { $_.PasswordLastSet -le $passwordtime } $SidHistory = $Computers.SIDHistory - $Categories = @('Total Computers', 'Dormant (> 90 days)','Password Age (> 30 days)','SidHistory') + $Categories = @('Total Computers', 'Dormant (> 90 days)', 'Password Age (> 30 days)', 'SidHistory') if ($Categories) { - Write-PscriboMessage "Collecting Status of Computer Accounts." + Write-PScriboMessage "Collecting Status of Computer Accounts." foreach ($Category in $Categories) { try { if ($Category -eq 'Total Computers') { $Values = $Computers - } - elseif ($Category -eq 'Dormant (> 90 days)') { + } elseif ($Category -eq 'Dormant (> 90 days)') { $Values = $Dormant - } - elseif ($Category -eq 'Password Age (> 30 days)') { + } elseif ($Category -eq 'Password Age (> 30 days)') { $Values = $PasswordAge - } - elseif ($Category -eq 'SidHistory') { + } elseif ($Category -eq 'SidHistory') { $Values = $SidHistory } $inObj = [ordered] @{ 'Category' = $Category 'Enabled' = ($Values.Enabled -eq $True | Measure-Object).Count 'Enabled %' = Switch ($Computers.Count) { - 0 {'0'} - $Null {'0'} - default {[math]::Round((($Values.Enabled -eq $True | Measure-Object).Count / $Computers.Count * 100), 2)} + 0 { '0' } + $Null { '0' } + default { [math]::Round((($Values.Enabled -eq $True | Measure-Object).Count / $Computers.Count * 100), 2) } } 'Disabled' = ($Values.Enabled -eq $False | Measure-Object).Count 'Disabled %' = Switch ($Computers.Count) { - 0 {'0'} - $Null {'0'} - default {[math]::Round((($Values.Enabled -eq $False | Measure-Object).Count / $Computers.Count * 100), 2)} + 0 { '0' } + $Null { '0' } + default { [math]::Round((($Values.Enabled -eq $False | Measure-Object).Count / $Computers.Count * 100), 2) } } 'Total' = ($Values | Measure-Object).Count 'Total %' = Switch ($Computers.Count) { - 0 {'0'} - $Null {'0'} - default {[math]::Round((($Values | Measure-Object).Count / $Computers.Count * 100), 2)} + 0 { '0' } + $Null { '0' } + default { [math]::Round((($Values | Measure-Object).Count / $Computers.Count * 100), 2) } } } $OutObj += [pscustomobject]$inobj - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Status of Computer Accounts)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Computer Accounts)" } } @@ -559,41 +581,40 @@ function Get-AbrADDomainObject { $addChartAreaParams = @{ Chart = $exampleChart - Name = 'exampleChartArea' + Name = 'exampleChartArea' } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru $addChartSeriesParams = @{ - Chart = $exampleChart - ChartArea = $exampleChartArea - Name = 'exampleChartSeries' - XField = 'Category' - YField = 'Total' - Palette = 'Blue' + 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' + Chart = $exampleChart + Name = 'Category' + TitleAlignment = 'Center' } Add-ChartLegend @addChartLegendParams $addChartTitleParams = @{ - Chart = $exampleChart + Chart = $exampleChart ChartArea = $exampleChartArea - Name = 'StatusofComputerAccounts' - Text = 'Computer Accounts' - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + 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) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } if ($OutObj) { @@ -605,30 +626,29 @@ function Get-AbrADDomainObject { } } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { Section -Style Heading4 'Operating Systems Count' { $OutObj = @() if ($Domain) { - Write-PscriboMessage "Collecting Operating Systems in Active Directory." + Write-PScriboMessage "Collecting Operating Systems in Active Directory." try { - $OSObjects = $Computers | Where-Object {$_.name -like '*' } | 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 {'No OS Specified'} - default {$OSObject.Name} + $True { 'No OS Specified' } + default { $OSObject.Name } } 'Count' = $OSObject.Count } $OutObj += [pscustomobject]$inobj } if ($HealthCheck.Domain.Security) { - $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*'} | Set-Style -Style Critical -Property 'Operating System' + $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*' } | Set-Style -Style Critical -Property 'Operating System' } $TableParams = @{ @@ -640,7 +660,7 @@ function Get-AbrADDomainObject { $TableParams['Caption'] = "- $($TableParams.Name)" } $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*'})) { + 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:" -Bold -Underline BlankLine Paragraph { @@ -649,23 +669,21 @@ function Get-AbrADDomainObject { } } } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Operating Systems in Active Directory)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Operating Systems in Active Directory)" } } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { Section -Style Heading4 'Default Domain Password Policy' { $OutObj = @() if ($Domain) { - Write-PscriboMessage "Collecting the Active Directory Default Domain Password Policy of domain $Item." + Write-PScriboMessage "Collecting the Active Directory Default Domain Password Policy of domain $Item." try { - $PasswordPolicy = Invoke-Command -Session $TempPssSession {Get-ADDefaultDomainPasswordPolicy -Identity $using:Domain} + $PasswordPolicy = Invoke-Command -Session $TempPssSession { Get-ADDefaultDomainPasswordPolicy -Identity $using:Domain } if ($PasswordPolicy) { $inObj = [ordered] @{ 'Password Must Meet Complexity Requirements' = ConvertTo-TextYN $PasswordPolicy.ComplexityEnabled @@ -691,22 +709,20 @@ function Get-AbrADDomainObject { } $OutObj | Table @TableParams } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Default Domain Password Policy)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Default Domain Password Policy)" } } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } try { if ($Domain) { foreach ($Item in $Domain) { - Write-PscriboMessage "Collecting the Active Directory Fined Grained Password Policies of domain $Item." - $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 + Write-PScriboMessage "Collecting the Active Directory Fined Grained Password Policies of domain $Item." + $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 = @() @@ -714,7 +730,7 @@ function Get-AbrADDomainObject { try { $Accounts = @() foreach ($ADObject in $FGPP.AppliesTo) { - $Accounts += Invoke-Command -Session $TempPssSession {Get-ADObject $using:ADObject -Server $using:DC -Properties sAMAccountName | 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 @@ -733,9 +749,8 @@ function Get-AbrADDomainObject { 'Applies To' = $Accounts -join ", " } $FGPPInfo += [pscustomobject]$inobj - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } @@ -769,18 +784,17 @@ function Get-AbrADDomainObject { } } } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Fined Grained Password Policies)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Fined Grained Password Policies)" } try { if ($Domain -eq $ADSystem.RootDomain) { 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} - $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 + Write-PScriboMessage "Collecting the Active Directory LAPS Policies from domain $Item." + $DomainInfo = Invoke-Command -Session $TempPssSession { Get-ADDomain $using:Domain -ErrorAction Stop } + $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 { @@ -788,8 +802,8 @@ function Get-AbrADDomainObject { 'Name' = $LAPS.Name 'Domain Name' = $Item 'Enabled' = Switch ($LAPS.Count) { - 0 {'No'} - default {'Yes'} + 0 { 'No' } + default { 'Yes' } } 'Distinguished Name' = $LAPS.DistinguishedName @@ -800,9 +814,8 @@ function Get-AbrADDomainObject { $LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Warning } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } if ($InfoLevel.Domain -ge 2) { @@ -830,7 +843,7 @@ function Get-AbrADDomainObject { $LAPSInfo | Table @TableParams } - if ($HealthCheck.Domain.Security -and ($LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' })) { + if ($HealthCheck.Domain.Security -and ($LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' })) { Paragraph "Health Check:" -Bold -Underline BlankLine Paragraph { @@ -841,9 +854,8 @@ function Get-AbrADDomainObject { } } } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Local Administrator Password Solution)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Local Administrator Password Solution)" } try { @@ -851,7 +863,7 @@ function Get-AbrADDomainObject { Write-PScriboMessage "Collecting the Active Directory Group Managed Service Accounts for $Item." try { 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 *} + $GMSA = Invoke-Command -Session $TempPssSession { Get-ADServiceAccount -Server $using:DC -Filter * -Properties * } if ($GMSA) { Section -Style Heading4 'gMSA identities' { $GMSAInfo = @() @@ -870,23 +882,22 @@ function Get-AbrADDomainObject { 'Locked Out' = ConvertTo-TextYN $Account.LockedOut 'Logon Count' = $Account.logonCount 'Password Expired' = ConvertTo-TextYN $Account.PasswordExpired - 'Password Last Set' = $Account.PasswordLastSet + 'Password Last Set' = $Account.PasswordLastSet } $GMSAInfo += [pscustomobject]$inobj if ($HealthCheck.Domain.GMSA) { $GMSAInfo | Where-Object { $_.'Enabled' -ne 'Yes' } | Set-Style -Style Warning -Property 'Enabled' - $GMSAInfo | Where-Object {$_.'Password Last Set' -lt (Get-Date).adddays(-60)} | Set-Style -Style Warning -Property 'Password Last Set' - $GMSAInfo | Where-Object {$_.'Last Logon Date' -lt (Get-Date).adddays(-60) -or $_.'Last Logon Date' -eq '--'} | Set-Style -Style Warning -Property 'Last Logon Date' - $GMSAInfo | Where-Object { $_.'Locked Out' -eq 'Yes'} | Set-Style -Style Warning -Property 'Locked Out' + $GMSAInfo | Where-Object { $_.'Password Last Set' -lt (Get-Date).adddays(-60) } | Set-Style -Style Warning -Property 'Password Last Set' + $GMSAInfo | Where-Object { $_.'Last Logon Date' -lt (Get-Date).adddays(-60) -or $_.'Last Logon Date' -eq '--' } | Set-Style -Style Warning -Property 'Last Logon Date' + $GMSAInfo | Where-Object { $_.'Locked Out' -eq 'Yes' } | Set-Style -Style Warning -Property 'Locked Out' $GMSAInfo | Where-Object { $_.'Logon Count' -eq 0 } | Set-Style -Style Warning -Property 'Logon Count' $GMSAInfo | Where-Object { $_.'Password Expired' -eq 'Yes' } | Set-Style -Style Warning -Property 'Password Expired' $GMSAInfo | Where-Object { $_.'Host Computers' -eq '--' } | Set-Style -Style Warning -Property 'Host Computers' $GMSAInfo | Where-Object { $_.'Retrieve Managed Password' -eq '--' } | Set-Style -Style Warning -Property 'Retrieve Managed Password' } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts)" } } @@ -918,14 +929,12 @@ function Get-AbrADDomainObject { } } } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts)" } } - } - catch { - Write-PscriboMessage -IsWarning $($_.Exception.Message) + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } } From a3609826d9ea429339b71640ce3fad4574c0e612 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 22 Jul 2023 19:54:40 -0400 Subject: [PATCH 17/22] Add Privileged User Groups #126 --- Src/Private/Get-AbrADDomainObject.ps1 | 89 +++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index 2dbd889..05b1511 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -417,6 +417,7 @@ function Get-AbrADDomainObject { } if ($GroupsSID) { if ($InfoLevel.Domain -eq 1) { + Paragraph "The following session summarizes the counts of users within the privileged groups." foreach ($GroupSID in $GroupsSID) { try { $Group = Invoke-Command -Session $TempPssSession { Get-ADGroup -Server $using:DC -Filter * | Where-Object { $_.SID -like $using:GroupSID } } @@ -435,7 +436,18 @@ function Get-AbrADDomainObject { } if ($HealthCheck.Domain.Security) { - $OutObj | Where-Object { $_.'Group Name' -eq 'Schema Admins' -and $_.Count -gt 1 } | Set-Style -Style Warning + foreach ( $OBJ in ($OutObj | Where-Object {$_.'Group Name' -eq 'Schema Admins' -and $_.Count -gt 1})) { + $OBJ.'Group Name' = "*" + $OBJ.'Group Name' + } + foreach ( $OBJ in ($OutObj | Where-Object {$_.'Group Name' -eq 'Enterprise Admins' -and $_.Count -gt 1})) { + $OBJ.'Group Name' = "**" + $OBJ.'Group Name' + } + foreach ( $OBJ in ($OutObj | Where-Object {$_.'Group Name' -eq 'Domain Admins' -and $_.Count -gt 5})) { + $OBJ.'Group Name' = "***" + $OBJ.'Group Name' + } + $OutObj | Where-Object { $_.'Group Name' -eq '*Schema Admins' -and $_.Count -gt 1 } | Set-Style -Style Warning + $OutObj | Where-Object { $_.'Group Name' -eq '**Enterprise Admins' -and $_.Count -gt 1 } | Set-Style -Style Warning + $OutObj | Where-Object { $_.'Group Name' -eq '***Domain Admins' -and $_.Count -gt 5 } | Set-Style -Style Warning } $TableParams = @{ @@ -447,28 +459,47 @@ function Get-AbrADDomainObject { $TableParams['Caption'] = "- $($TableParams.Name)" } $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 })) { + if ($HealthCheck.Domain.Security -and ($OutObj | Where-Object { $_.'Group Name' -eq '*Schema Admins' -and $_.Count -gt 1 }) -or ($OutObj | Where-Object { $_.'Group Name' -eq '**Enterprise Admins' -and $_.Count -gt 1 }) -or ($OutObj | Where-Object { $_.'Group Name' -eq '***Domain Admins' -and $_.Count -gt 5 })) { Paragraph "Health Check:" -Bold -Underline BlankLine - Paragraph { - Text "Security Best Practice:" -Bold - Text "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." + Paragraph "Security Best Practice:" -Bold + if ($OutObj | Where-Object { $_.'Group Name' -eq '*Schema Admins' -and $_.Count -gt 1 }) { + BlankLine + Paragraph { + Text "*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." + } + } + if ($OutObj | Where-Object { $_.'Group Name' -eq '**Enterprise Admins' -and $_.Count -gt 1 }) { + BlankLine + Paragraph { + Text "**Unless an account is doing specific tasks needing those highly elevated permissions, every account should be removed from Enterprise Admins (EA) group. A side benefit of having an empty Enterprise Admins group is that it adds just enough friction to ensure that enterprise-wide changes requiring Enterprise Admin rights are done purposefully and methodically." + } + } + if ($OutObj | Where-Object { $_.'Group Name' -eq '***Domain Admins' -and $_.Count -gt 5 }) { + BlankLine + Paragraph { + Text "***Microsoft recommends that Domain Admins contain no more than five members." + } } } } else { + Paragraph "The following session details the members users within the privilege groups." foreach ($GroupSID in $GroupsSID) { try { $Group = Invoke-Command -Session $TempPssSession { Get-ADGroup -Server $using:DC -Filter * | Where-Object { $_.SID -like $using:GroupSID } } if ($Group) { Write-PScriboMessage "Collecting Privileged Group $($Group.Name) with SID $($Group.SID)" - $GroupObjects = Invoke-Command -Session $TempPssSession { Get-ADGroupMember -Server $using:DC -Identity ($using:Group).Name -Recursive -ErrorAction SilentlyContinue | ForEach-Object {Get-ADUser -Filter 'SamAccountName -eq $_.SamAccountName' -Server $using:DC -Property SamAccountName,objectClass,PasswordLastSet,passwordNeverExpires,Enabled -SearchBase (Get-ADDomain -Identity $using:Domain).distinguishedName }} + $GroupObjects = Invoke-Command -Session $TempPssSession { Get-ADGroupMember -Server $using:DC -Identity ($using:Group).Name -Recursive -ErrorAction SilentlyContinue | ForEach-Object {Get-ADUser -Filter 'SamAccountName -eq $_.SamAccountName' -Server $using:DC -Property SamAccountName,objectClass,LastLogonDate,passwordNeverExpires,Enabled -SearchBase (Get-ADDomain -Identity $using:Domain).distinguishedName }} if ($GroupObjects) { Section -ExcludeFromTOC -Style NOTOCHeading5 "$($Group.Name) ($(($GroupObjects | Measure-Object).count) Members)" { $OutObj = @() foreach ($GroupObject in $GroupObjects) { $inObj = [ordered] @{ 'Name' = $GroupObject.SamAccountName - 'Password Last Changed' = $GroupObject.PasswordLastSet + 'Last Logon Date' = switch ($GroupObject.LastLogonDate) { + $null {"--"} + default {$GroupObject.LastLogonDate.ToShortDateString()} + } 'Password Never Expires' = ConvertTo-TextYN $GroupObject.passwordNeverExpires 'Account Enabled' = ConvertTo-TextYN $GroupObject.Enabled } @@ -477,8 +508,14 @@ function Get-AbrADDomainObject { if ($HealthCheck.Domain.Security) { $OutObj | Where-Object { $_.'Password Never Expires' -eq 'Yes' } | Set-Style -Style Warning -Property 'Password Never Expires' + foreach ( $OBJ in ($OutObj | Where-Object {$_.'Password Never Expires' -eq 'Yes'})) { + $OBJ.'Password Never Expires' = "**Yes" + } $OutObj | Where-Object { $_.'Account Enabled' -eq 'No' } | Set-Style -Style Warning -Property 'Account Enabled' - $OutObj | Where-Object { $_.'Password Last Changed' -le (Get-Date).AddDays(-90) } | Set-Style -Style Warning -Property 'Password Last Changed' + $OutObj | Where-Object { $_.'Last Logon Date' -ne "--" -and [DateTime]$_.'Last Logon Date' -le (Get-Date).AddDays(-90) } | Set-Style -Style Warning -Property 'Last Logon Date' + foreach ( $OBJ in ($OutObj | Where-Object { $_.'Last Logon Date' -ne "--" -and [DateTime]$_.'Last Logon Date' -le (Get-Date).AddDays(-90) })) { + $OBJ.'Last Logon Date' = "*" + $OBJ.'Last Logon Date' + } } $TableParams = @{ @@ -490,12 +527,40 @@ function Get-AbrADDomainObject { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams - if ($HealthCheck.Domain.Security -and ($Group.Name -eq 'Schema Admins') -and ($GroupObjects | Measure-Object).count -gt 0) { + if ($HealthCheck.Domain.Security -and ((($Group.Name -eq 'Schema Admins') -and ($GroupObjects | Measure-Object).count -gt 0) -or ($Group.Name -eq 'Enterprise Admins') -and ($GroupObjects | Measure-Object).count -gt 0) -or (($Group.Name -eq 'Domain Admins') -and ($GroupObjects | Measure-Object).count -gt 5) -or ($OutObj | Where-Object { $_.'Password Never Expires' -eq '**Yes' }) -or ($OutObj | Where-Object { $_.'Last Logon Date' -ne "--" -and $_.'Last Logon Date' -match "\*" })) { Paragraph "Health Check:" -Bold -Underline BlankLine - Paragraph { - Text "Security Best Practice:" -Bold - Text "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." + Paragraph "Security Best Practice:" -Bold + + if (($Group.Name -eq 'Schema Admins') -and ($GroupObjects | Measure-Object).count -gt 0) { + BlankLine + Paragraph { + Text "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." + } + } + if (($Group.Name -eq 'Enterprise Admins') -and ($GroupObjects | Measure-Object).count -gt 0) { + BlankLine + Paragraph { + Text "Unless an account is doing specific tasks needing those highly elevated permissions, every account should be removed from Enterprise Admins (EA) group. A side benefit of having an empty Enterprise Admins group is that it adds just enough friction to ensure that enterprise-wide changes requiring Enterprise Admin rights are done purposefully and methodically." + } + } + if (($Group.Name -eq 'Domain Admins') -and ($GroupObjects | Measure-Object).count -gt 5) { + BlankLine + Paragraph { + Text "Microsoft recommends that the Domain Admins group contain no more than five members." + } + } + if ($OutObj | Where-Object { $_.'Password Never Expires' -eq '**Yes' }) { + BlankLine + Paragraph { + Text "**Ensure there aren't any account with weak security posture." + } + } + if ($OutObj | Where-Object { $_.'Last Logon Date' -match "\*" }) { + BlankLine + Paragraph { + Text "*Regularly check for and remove inactive privileged user accounts in Active Directory." + } } } } From 0d870114d55b4f461fd81ff617192f53c902f505 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 22 Jul 2023 19:59:00 -0400 Subject: [PATCH 18/22] Add File Shares on a Domain Controller health check #125 --- Src/Private/Get-AbrADDomainController.ps1 | 129 +++++++++++----------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index c8c925f..9a57d73 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -293,71 +293,6 @@ function Get-AbrADDomainController { Write-PscriboMessage -IsWarning "Domain Controller DNS IP Configuration Section: $($_.Exception.Message)" } - try { - if ($HealthCheck.DomainController.BestPractice) { - Write-PscriboMessage "Discovering Active Directory File Shares information from $Domain." - $OutObj = foreach ($DC in $DCs) { - if (Test-Connection -ComputerName $DC -Quiet -Count 2) { - try { - Write-PscriboMessage "Collecting AD Domain Controllers file shares information of $DC." - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllersFileShares' - $Shares = Invoke-Command -Session $DCPssSession { Get-SmbShare | Where-Object { $_.Description -ne 'Default share' -and $_.Description -notmatch 'Remote' -and $_.Name -ne 'NETLOGON' -and $_.Name -ne 'SYSVOL' } } - if ($Shares) { - Section -ExcludeFromTOC -Style NOTOCHeading6 $($DC.ToString().ToUpper().Split(".")[0]) { - $FSObj = @() - foreach ($Share in $Shares) { - $inObj = [ordered] @{ - 'Name' = $Share.Name - 'Path' = $Share.Path - 'Description' = ConvertTo-EmptyToFiller $Share.Description - } - $FSObj += [pscustomobject]$inobj - } - - if ($HealthCheck.DomainController.BestPractice) { - $FSObj | Set-Style -Style Warning - } - - $TableParams = @{ - Name = "File Shares - $($DC.ToString().ToUpper().Split(".")[0])" - List = $false - ColumnWidths = 34, 33, 33 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - - $FSObj | Sort-Object -Property 'Name' | Table @TableParams - } - } - if ($DCPssSession) { - Remove-PSSession -Session $DCPssSession - } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (File Shares Item)" - } - } - } - - if ($OutObj) { - Section -Style Heading5 "File Shares" { - Paragraph "The following domain controllers have non-default file shares." - $OutObj - Paragraph "Health Check:" -Bold -Underline - BlankLine - Paragraph { - Text "Best Practice:" -Bold - Text "Only netlogon, sysvol and the default administrative shares should exist on a Domain Controller. If possible, non default file shares should be moved to another server, preferably a dedicated file server. " - } - } - } - } - } - catch { - Write-PscriboMessage -IsWarning "$($_.Exception.Message) (File Shares Table)" - } - try { Write-PscriboMessage "Collecting AD Domain Controller NTDS information." Section -Style Heading5 'NTDS Information' { @@ -575,6 +510,70 @@ function Get-AbrADDomainController { Write-PscriboMessage -IsWarning "$($_.Exception.Message) (SRV Records Status)" } } + try { + if ($HealthCheck.DomainController.BestPractice) { + Write-PscriboMessage "Discovering Active Directory File Shares information from $Domain." + $OutObj = foreach ($DC in $DCs) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { + try { + Write-PscriboMessage "Collecting AD Domain Controllers file shares information of $DC." + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DomainControllersFileShares' + $Shares = Invoke-Command -Session $DCPssSession { Get-SmbShare | Where-Object { $_.Description -ne 'Default share' -and $_.Description -notmatch 'Remote' -and $_.Name -ne 'NETLOGON' -and $_.Name -ne 'SYSVOL' } } + if ($Shares) { + Section -ExcludeFromTOC -Style NOTOCHeading6 $($DC.ToString().ToUpper().Split(".")[0]) { + $FSObj = @() + foreach ($Share in $Shares) { + $inObj = [ordered] @{ + 'Name' = $Share.Name + 'Path' = $Share.Path + 'Description' = ConvertTo-EmptyToFiller $Share.Description + } + $FSObj += [pscustomobject]$inobj + } + + if ($HealthCheck.DomainController.BestPractice) { + $FSObj | Set-Style -Style Warning + } + + $TableParams = @{ + Name = "File Shares - $($DC.ToString().ToUpper().Split(".")[0])" + List = $false + ColumnWidths = 34, 33, 33 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + + $FSObj | Sort-Object -Property 'Name' | Table @TableParams + } + } + if ($DCPssSession) { + Remove-PSSession -Session $DCPssSession + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (File Shares Item)" + } + } + } + + if ($OutObj) { + Section -Style Heading5 "File Shares" { + Paragraph "The following domain controllers have non-default file shares." + $OutObj + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "Only netlogon, sysvol and the default administrative shares should exist on a Domain Controller. If possible, non default file shares should be moved to another server, preferably a dedicated file server. " + } + } + } + } + } + catch { + Write-PscriboMessage -IsWarning "$($_.Exception.Message) (File Shares Table)" + } if ($HealthCheck.DomainController.Software) { try { Write-PscriboMessage "Collecting additional software running on the Domain Controller." From 69637ef0d83c461dc5e46092fd06f137fb9cabaf Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 24 Jul 2023 21:26:27 -0400 Subject: [PATCH 19/22] Add gMSA identities health check #124 --- Src/Private/Get-AbrADDomainObject.ps1 | 87 ++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index 05b1511..dec40d2 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -937,32 +937,56 @@ function Get-AbrADDomainObject { $inObj = [ordered] @{ 'Name' = $Account.Name 'SamAccountName' = $Account.SamAccountName - 'Created' = $Account.Created + 'Created' = Switch ($Account.Created) { + $null {'--'} + default {$Account.Created.ToShortDateString()} + } 'Enabled' = ConvertTo-TextYN $Account.Enabled 'DNS Host Name' = $Account.DNSHostName 'Host Computers' = ConvertTo-EmptyToFiller ((ConvertTo-ADObjectName -DN $Account.HostComputers -Session $TempPssSession -DC $DC) -join ", ") 'Retrieve Managed Password' = ConvertTo-EmptyToFiller ((ConvertTo-ADObjectName $Account.PrincipalsAllowedToRetrieveManagedPassword -Session $TempPssSession -DC $DC) -join ", ") 'Primary Group' = (ConvertTo-ADObjectName $Account.PrimaryGroup -Session $TempPssSession -DC $DC) -join ", " - 'Last Logon Date' = ConvertTo-EmptyToFiller $Account.LastLogonDate + 'Last Logon Date' = Switch ($Account.LastLogonDate) { + $null {'--'} + default {$Account.LastLogonDate.ToShortDateString()} + } 'Locked Out' = ConvertTo-TextYN $Account.LockedOut 'Logon Count' = $Account.logonCount 'Password Expired' = ConvertTo-TextYN $Account.PasswordExpired - 'Password Last Set' = $Account.PasswordLastSet + 'Password Last Set' = Switch ($Account.PasswordLastSet) { + $null {'--'} + default {$Account.PasswordLastSet.ToShortDateString()} + } } $GMSAInfo += [pscustomobject]$inobj - if ($HealthCheck.Domain.GMSA) { - $GMSAInfo | Where-Object { $_.'Enabled' -ne 'Yes' } | Set-Style -Style Warning -Property 'Enabled' - $GMSAInfo | Where-Object { $_.'Password Last Set' -lt (Get-Date).adddays(-60) } | Set-Style -Style Warning -Property 'Password Last Set' - $GMSAInfo | Where-Object { $_.'Last Logon Date' -lt (Get-Date).adddays(-60) -or $_.'Last Logon Date' -eq '--' } | Set-Style -Style Warning -Property 'Last Logon Date' - $GMSAInfo | Where-Object { $_.'Locked Out' -eq 'Yes' } | Set-Style -Style Warning -Property 'Locked Out' - $GMSAInfo | Where-Object { $_.'Logon Count' -eq 0 } | Set-Style -Style Warning -Property 'Logon Count' - $GMSAInfo | Where-Object { $_.'Password Expired' -eq 'Yes' } | Set-Style -Style Warning -Property 'Password Expired' - $GMSAInfo | Where-Object { $_.'Host Computers' -eq '--' } | Set-Style -Style Warning -Property 'Host Computers' - $GMSAInfo | Where-Object { $_.'Retrieve Managed Password' -eq '--' } | Set-Style -Style Warning -Property 'Retrieve Managed Password' - } } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts)" + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts Item)" + } + } + + if ($HealthCheck.Domain.GMSA) { + $GMSAInfo | Where-Object { $_.'Enabled' -ne 'Yes' } | Set-Style -Style Warning -Property 'Enabled' + $GMSAInfo | Where-Object { $_.'Password Last Set' -ne '--' -and [datetime]$_.'Password Last Set' -lt (Get-Date).adddays(-60) } | Set-Style -Style Warning -Property 'Password Last Set' + $GMSAInfo | Where-Object { $_.'Password Last Set' -eq '--' } | Set-Style -Style Warning -Property 'Password Last Set' + $GMSAInfo | Where-Object { $_.'Last Logon Date' -ne '--' -and [datetime]$_.'Last Logon Date' -lt (Get-Date).adddays(-60) } | Set-Style -Style Warning -Property 'Last Logon Date' + $GMSAInfo | Where-Object { $_.'Last Logon Date' -eq '--' } | Set-Style -Style Warning -Property 'Last Logon Date' + foreach ( $OBJ in ($GMSAInfo | Where-Object { $_.'Last Logon Date' -eq '--' })) { + $OBJ.'Last Logon Date' = "*" + $OBJ.'Last Logon Date' + } + foreach ( $OBJ in ($GMSAInfo | Where-Object { $_.'Last Logon Date' -ne '*--' -and [datetime]$_.'Last Logon Date' -lt (Get-Date).adddays(-60) })) { + $OBJ.'Last Logon Date' = "*" + $OBJ.'Last Logon Date' + } + $GMSAInfo | Where-Object { $_.'Locked Out' -eq 'Yes' } | Set-Style -Style Warning -Property 'Locked Out' + $GMSAInfo | Where-Object { $_.'Logon Count' -eq 0 } | Set-Style -Style Warning -Property 'Logon Count' + $GMSAInfo | Where-Object { $_.'Password Expired' -eq 'Yes' } | Set-Style -Style Warning -Property 'Password Expired' + $GMSAInfo | Where-Object { $_.'Host Computers' -eq '--' } | Set-Style -Style Warning -Property 'Host Computers' + foreach ( $OBJ in ($GMSAInfo | Where-Object { $_.'Host Computers' -eq '--' })) { + $OBJ.'Host Computers' = "**" + $OBJ.'Host Computers' + } + $GMSAInfo | Where-Object { $_.'Retrieve Managed Password' -eq '--' } | Set-Style -Style Warning -Property 'Retrieve Managed Password' + foreach ( $OBJ in ($GMSAInfo | Where-Object { $_.'Retrieve Managed Password' -eq '--' })) { + $OBJ.'Retrieve Managed Password' = "***" + $OBJ.'Retrieve Managed Password' } } @@ -978,6 +1002,29 @@ function Get-AbrADDomainObject { $TableParams['Caption'] = "- $($TableParams.Name)" } $Account | Table @TableParams + if (($Account | Where-Object { $_.'Last Logon Date' -ne '*--' -or $_.'Enabled' -ne 'Yes' -or ($_.'Last Logon Date' -eq '--')}) -or ($Account | Where-Object { $_.'Host Computers' -eq '**--' }) -or ($Account | Where-Object { $_.'Retrieve Managed Password' -eq '**--' })) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph "Security Best Practice:" -Bold + if ($Account | Where-Object { $_.'Last Logon Date' -ne '*--' -or $_.'Enabled' -ne 'Yes' -or ($_.'Last Logon Date' -eq '*--') }) { + BlankLine + Paragraph { + Text "*Regularly check for and remove inactive group managed service accounts from Active Directory." + } + } + if ($Account | Where-Object { $_.'Host Computers' -eq '**--' }) { + BlankLine + Paragraph { + Text "**No 'Host Computers' has been defined, please validate that the gMSA is currently in use. If not, it is recommended to remove these unused resources from Active Directory." + } + } + if ($Account | Where-Object { $_.'Retrieve Managed Password' -eq '***--' }) { + BlankLine + Paragraph { + Text "***No 'Retrieve Managed Password' has been defined, please validate that the gMSA is currently in use. If not, it is recommended to remove these unused resources from Active Directory." + } + } + } } } } else { @@ -991,11 +1038,21 @@ function Get-AbrADDomainObject { $TableParams['Caption'] = "- $($TableParams.Name)" } $GMSAInfo | Table @TableParams + if (($GMSAInfo | Where-Object { $_.'Last Logon Date' -ne '--' -and [datetime]$_.'Last Logon Date' -lt (Get-Date).adddays(-60) -or $_.'Enabled' -ne 'Yes' -or ($_.'Last Logon Date' -eq '--')})) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($GMSAInfo | Where-Object { $_.'Last Logon Date' -match "\*" }) { + Paragraph { + Text "Security Best Practice:" -Bold + Text "*Regularly check for and remove inactive group managed service accounts from Active Directory." + } + } + } } } } } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts)" + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts Section)" } } } catch { From 89a0f06fb8a13d995f9f406fe001244b0651e0a9 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 25 Jul 2023 22:41:51 -0400 Subject: [PATCH 20/22] Add gMSA identities health check #124 --- Src/Private/Get-AbrADDomainObject.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index dec40d2..4d10b1e 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -1038,10 +1038,10 @@ function Get-AbrADDomainObject { $TableParams['Caption'] = "- $($TableParams.Name)" } $GMSAInfo | Table @TableParams - if (($GMSAInfo | Where-Object { $_.'Last Logon Date' -ne '--' -and [datetime]$_.'Last Logon Date' -lt (Get-Date).adddays(-60) -or $_.'Enabled' -ne 'Yes' -or ($_.'Last Logon Date' -eq '--')})) { + if (($GMSAInfo | Where-Object { $_.'Last Logon Date' -eq '*--' -or $_.'Enabled' -ne 'Yes' -or ($_.'Last Logon Date' -eq '--')})) { Paragraph "Health Check:" -Bold -Underline BlankLine - if ($GMSAInfo | Where-Object { $_.'Last Logon Date' -match "\*" }) { + if ($GMSAInfo | Where-Object { $_.'Last Logon Date' -eq "*--" }) { Paragraph { Text "Security Best Practice:" -Bold Text "*Regularly check for and remove inactive group managed service accounts from Active Directory." From d9bcc473e4c5b749edb976d385234593c6c0c74c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 25 Jul 2023 22:42:43 -0400 Subject: [PATCH 21/22] Added test-connection to verify DC connectivity --- Src/Private/Get-AbrADSite.ps1 | 129 +++++++++--------- .../Invoke-AsBuiltReport.Microsoft.AD.ps1 | 12 +- 2 files changed, 72 insertions(+), 69 deletions(-) diff --git a/Src/Private/Get-AbrADSite.ps1 b/Src/Private/Get-AbrADSite.ps1 index a1a5821..a515cad 100644 --- a/Src/Private/Get-AbrADSite.ps1 +++ b/Src/Private/Get-AbrADSite.ps1 @@ -214,36 +214,38 @@ function Get-AbrADSite { 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] - } + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { + try { + $DCPssSession = New-PsSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'MissingSubnetinAD' + $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 - } + $OutObj += [pscustomobject]$inobj + } - if ($HealthCheck.Site.BestPractice) { - $OutObj | Where-Object { $_.'Replication Status' -eq 'Normal' } | Set-Style -Style OK -Property 'Replication Status' + 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 } - } 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)" } } - catch { - Write-PscriboMessage -IsWarning "Missing Subnet in AD Item Section: $($_.Exception.Message)" - } } } if ($OutObj) { @@ -379,51 +381,52 @@ function Get-AbrADSite { 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 - } + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { + $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'} + 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 } - 'Domain' = $Domain + $OutObj += [pscustomobject]$inobj + } + catch { + Write-PscriboMessage -IsWarning "Sysvol Replication Item Section: $($_.Exception.Message)" } - $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 ($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) { diff --git a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 index 33c5254..6b71f09 100644 --- a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 +++ b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 @@ -186,7 +186,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { Paragraph "The following section provides a summary of the Active Directory Domain Controllers." BlankLine } - $DCs = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Where-Object { $_ -notin ($using:Options).Exclude.DCs}} + $DCs = Invoke-Command -Session $TempPssSession {Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Where-Object { $_ -notin ($using:Options).Exclude.DCs}} | Sort-Object if ($DCs) { @@ -196,7 +196,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { Section -Style Heading5 "Roles" { 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 + $DCStatus = Test-Connection -ComputerName $DC -Quiet -Count 2 if ($DCStatus -eq $false) { Write-PScriboMessage -IsWarning "Unable to connect to $DC. Removing it from the $Domain report" } @@ -212,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 2)) { Get-AbrADDCDiag -Domain $Domain -DC $DC } } @@ -228,7 +228,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { Section -Style Heading5 "Infrastructure Services" { 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 2)) { Get-AbrADInfrastructureService -DC $DC } } @@ -287,8 +287,8 @@ function Invoke-AsBuiltReport.Microsoft.AD { Get-AbrADDNSInfrastructure -Domain $Domain $DCs = Invoke-Command -Session $TempPssSession {Get-ADDomain $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers | Where-Object { $_ -notin ($using:Options).Exclude.DCs}} foreach ($DC in $DCs){ - if (Test-Connection -ComputerName $DC -Quiet -Count 1) { - $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { + $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DDNSInfrastructure' Get-AbrADDNSZone -Domain $Domain -DC $DC } if ($DCPssSession) { From c7b40928ac6531c6f65abb6bf939e32cfe07fe0c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 25 Jul 2023 22:51:24 -0400 Subject: [PATCH 22/22] Added v0.7.14 changes --- CHANGELOG.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b93f8..a9c6a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,21 @@ # :arrows_clockwise: Microsoft AD As Built Report Changelog -## [0.7.14] - 2023-07-22 - -### Added - -- - -### Changed - -- +## [0.7.14] - 2023-07-25 ### Fixed -- Fix [#128](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/128) -- Fix [#124](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/124) +- Resolve [#113](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/113) +- Resolve [#116](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/116) +- Resolve [#117](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/117) +- Resolve [#118](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/118) +- Resolve [#119](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/119) +- Resolve [#120](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/120) +- Resolve [#121](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/121) +- Resolve [#123](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/123) +- Resolve [#124](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/124) +- Resolve [#125](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/125) +- Resolve [#126](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/126) +- Resolve [#128](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/128) ## [0.7.13] - 2023-06-22