From 449fcc50a1b349c8f870195d79dd07459dc5e3c7 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 17 Feb 2024 00:45:26 -0400 Subject: [PATCH 01/17] Fix some DNS sections not getting information --- Src/Private/Get-AbrADDNSInfrastructure.ps1 | 118 +++++++++++---------- Src/Private/Get-AbrADDNSZone.ps1 | 12 +-- 2 files changed, 63 insertions(+), 67 deletions(-) diff --git a/Src/Private/Get-AbrADDNSInfrastructure.ps1 b/Src/Private/Get-AbrADDNSInfrastructure.ps1 index 24da866..c8e1f39 100644 --- a/Src/Private/Get-AbrADDNSInfrastructure.ps1 +++ b/Src/Private/Get-AbrADDNSInfrastructure.ps1 @@ -24,7 +24,7 @@ function Get-AbrADDNSInfrastructure { ) begin { - Write-PScriboMessage "Discovering Active Directory Domain Name System Infrastructure information for $Domain" + Write-PScriboMessage "Collecting Active Directory Domain Name System Infrastructure information for $Domain" } process { @@ -37,7 +37,6 @@ function Get-AbrADDNSInfrastructure { $OutObj = @() foreach ($DC in $DCs) { 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 $inObj = [ordered] @{ @@ -52,6 +51,8 @@ function Get-AbrADDNSInfrastructure { } catch { Write-PScriboMessage -IsWarning "DNS Infrastructure Summary Section: $($_.Exception.Message)" } + } else { + Write-PScriboMessage -IsWarning "DNS Infrastructure Summary Section: Unable to connect to DC server $DC" } } @@ -74,10 +75,9 @@ function Get-AbrADDNSInfrastructure { BlankLine foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { - Section -ExcludeFromTOC -Style NOTOCHeading5 $($DC.ToString().ToUpper().Split(".")[0]) { - $OutObj = @() - Write-PScriboMessage "Collecting Directory Partition information from $($DC)." - try { + try { + Section -ExcludeFromTOC -Style NOTOCHeading5 $($DC.ToString().ToUpper().Split(".")[0]) { + $OutObj = @() $DNSSetting = Get-DnsServerDirectoryPartition -CimSession $TempCIMSession -ComputerName $DC foreach ($Partition in $DNSSetting) { try { @@ -99,20 +99,21 @@ function Get-AbrADDNSInfrastructure { Write-PScriboMessage -IsWarning "Directory Partitions Item Section: $($_.Exception.Message)" } } - } catch { - Write-PScriboMessage -IsWarning "Directory Partitions Table Section: $($_.Exception.Message)" - } - - $TableParams = @{ - Name = "Directory Partitions - $($DC.ToString().ToUpper().Split(".")[0])" - List = $false - ColumnWidths = 40, 25, 25, 10 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" + $TableParams = @{ + Name = "Directory Partitions - $($DC.ToString().ToUpper().Split(".")[0])" + List = $false + ColumnWidths = 40, 25, 25, 10 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Name' | Table @TableParams } - $OutObj | Sort-Object -Property 'Name' | Table @TableParams + } catch { + Write-PScriboMessage -IsWarning "Directory Partitions Table Section: $($_.Exception.Message)" } + } else { + Write-PScriboMessage -IsWarning "DNS Directory Partition Section: Unable to connect to DC server $DC" } } } @@ -129,7 +130,6 @@ function Get-AbrADDNSInfrastructure { $OutObj = @() foreach ($DC in $DCs) { 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 $inObj = [ordered] @{ @@ -146,6 +146,8 @@ function Get-AbrADDNSInfrastructure { } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Response Rate Limiting (RRL) Item)" } + } else { + Write-PScriboMessage -IsWarning "DNS Response Rate Limiting (RRL) Section: Unable to connect to DC server $DC" } } @@ -172,7 +174,6 @@ function Get-AbrADDNSInfrastructure { $OutObj = @() foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { - Write-PScriboMessage "Collecting Scavenging Options information from $($DC)." try { $DNSSetting = Get-DnsServerScavenging -CimSession $TempCIMSession -ComputerName $DC $inObj = [ordered] @{ @@ -195,6 +196,8 @@ function Get-AbrADDNSInfrastructure { } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Scavenging Item)" } + } else { + Write-PScriboMessage -IsWarning "DNS Scavenging Section: Unable to connect to DC server $DC" } } @@ -232,7 +235,6 @@ function Get-AbrADDNSInfrastructure { $OutObj = @() foreach ($DC in $DCs) { 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 @@ -247,6 +249,8 @@ function Get-AbrADDNSInfrastructure { } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Forwarder Item)" } + } else { + Write-PScriboMessage -IsWarning "DNS Forwarder Section: Unable to connect to DC server $DC" } } @@ -301,10 +305,9 @@ function Get-AbrADDNSInfrastructure { BlankLine foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { - Section -ExcludeFromTOC -Style NOTOCHeading5 $($DC.ToString().ToUpper().Split(".")[0]) { - $OutObj = @() - Write-PScriboMessage "Collecting Root Hint information from $($DC)." - try { + try { + Section -ExcludeFromTOC -Style NOTOCHeading5 $($DC.ToString().ToUpper().Split(".")[0]) { + $OutObj = @() $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) { @@ -353,47 +356,49 @@ function Get-AbrADDNSInfrastructure { } } - } 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' - } + 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 - $($DC.ToString().ToUpper().Split(".")[0])" - List = $false - 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." - } + $TableParams = @{ + Name = "Root Hints - $($DC.ToString().ToUpper().Split(".")[0])" + List = $false + ColumnWidths = 40, 30, 30 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" } - 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 table 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." + $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 table 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." + } } } } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Root Hints Table)" } + } else { + Write-PScriboMessage -IsWarning "DNS Root Hints Section: Unable to connect to DC server $DC" } } } } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Root Hints Table)" + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Root Hints Section)" } } #---------------------------------------------------------------------------------------------# @@ -405,7 +410,6 @@ function Get-AbrADDNSInfrastructure { $OutObj = @() foreach ($DC in $DCs) { 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 $inObj = [ordered] @{ @@ -421,6 +425,8 @@ function Get-AbrADDNSInfrastructure { } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Zone Scope Recursion Item)" } + } else { + Write-PScriboMessage -IsWarning "DNS Zone Scope Recursion Section: Unable to connect to DC server $DC" } } diff --git a/Src/Private/Get-AbrADDNSZone.ps1 b/Src/Private/Get-AbrADDNSZone.ps1 index 62225bf..cb448af 100644 --- a/Src/Private/Get-AbrADDNSZone.ps1 +++ b/Src/Private/Get-AbrADDNSZone.ps1 @@ -26,7 +26,7 @@ function Get-AbrADDNSZone { ) begin { - Write-PScriboMessage "Discovering Actve Directory Domain Name System Zone information on $Domain." + Write-PScriboMessage "Collecting Actve Directory Domain Name System Zone information on $Domain." } process { @@ -35,10 +35,8 @@ function Get-AbrADDNSZone { if ($DNSSetting) { Section -Style Heading3 "$($DC.ToString().ToUpper().Split(".")[0]) DNS Zones" { $OutObj = @() - Write-PScriboMessage "Discovered Actve Directory Domain Controller: $DC. (Domain Name System Zone)" foreach ($Zones in $DNSSetting) { try { - Write-PScriboMessage "Collecting Actve Directory DNS Zone: '$($Zones.ZoneName)' on $DC" $inObj = [ordered] @{ 'Zone Name' = ConvertTo-EmptyToFiller $Zones.ZoneName 'Zone Type' = ConvertTo-EmptyToFiller $Zones.ZoneType @@ -74,7 +72,6 @@ function Get-AbrADDNSZone { if ($Delegations) { foreach ($Delegation in $Delegations) { try { - Write-PScriboMessage "Collecting Actve Directory DNS Zone: '$($Delegation.ZoneName)' on $DC" $inObj = [ordered] @{ 'Zone Name' = $Delegation.ZoneName 'Child Zone' = $Delegation.ChildZoneName @@ -124,7 +121,6 @@ function Get-AbrADDNSZone { $OutObj = @() foreach ($Zone in $DNSSetting) { try { - Write-PScriboMessage "Collecting Actve Directory DNS Zone: '$($Zone.PSChildName)' on $DC" $inObj = [ordered] @{ 'Zone Name' = $Zone.PSChildName 'Secondary Servers' = ConvertTo-EmptyToFiller ($Zone.SecondaryServers -join ", ") @@ -177,10 +173,8 @@ function Get-AbrADDNSZone { if ($DNSSetting) { Section -Style Heading4 "Reverse Lookup Zone" { $OutObj = @() - Write-PScriboMessage "Discovered Actve Directory Domain Controller: $DC (Domain Name System Zone)" foreach ($Zones in $DNSSetting) { try { - Write-PScriboMessage "Collecting Actve Directory DNS Zone: '$($Zones.ZoneName)' on $DC" $inObj = [ordered] @{ 'Zone Name' = ConvertTo-EmptyToFiller $Zones.ZoneName 'Zone Type' = ConvertTo-EmptyToFiller $Zones.ZoneType @@ -217,10 +211,8 @@ function Get-AbrADDNSZone { if ($DNSSetting) { Section -Style Heading4 "Conditional Forwarder" { $OutObj = @() - Write-PScriboMessage "Discovered Actve Directory Domain Controller: $DC. (Domain Name System Conditional Forwarder)" foreach ($Zones in $DNSSetting) { try { - Write-PScriboMessage "Collecting Actve Directory DNS Zone: '$($Zones.ZoneName)' on $DC" $inObj = [ordered] @{ 'Zone Name' = $Zones.ZoneName 'Zone Type' = $Zones.ZoneType @@ -252,7 +244,6 @@ function Get-AbrADDNSZone { } if ($InfoLevel.DNS -ge 2) { try { - Write-PScriboMessage "Discovered Actve Directory Domain Controller: $DC. (Domain Name System Zone)" $DNSSetting = Get-DnsServerZone -CimSession $TempCIMSession -ComputerName $DC | Where-Object { $_.IsReverseLookupZone -like "False" -and $_.ZoneType -eq "Primary" } | Select-Object -ExpandProperty ZoneName $Zones = Get-DnsServerZoneAging -CimSession $TempCIMSession -Name $DNSSetting -ComputerName $DC if ($Zones) { @@ -260,7 +251,6 @@ function Get-AbrADDNSZone { $OutObj = @() foreach ($Settings in $Zones) { try { - Write-PScriboMessage "Collecting Actve Directory DNS Zone: '$($Settings.ZoneName)' on $DC" $inObj = [ordered] @{ 'Zone Name' = ConvertTo-EmptyToFiller $Settings.ZoneName 'Aging Enabled' = ConvertTo-EmptyToFiller (ConvertTo-TextYN $Settings.AgingEnabled) From c1ae716b67b16cb9332c977d1389025b95be949f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 17 Feb 2024 00:46:02 -0400 Subject: [PATCH 02/17] Increased Diagrammer.Microsoft.AD module version requirement --- AsBuiltReport.Microsoft.AD.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsBuiltReport.Microsoft.AD.psd1 b/AsBuiltReport.Microsoft.AD.psd1 index 7fb0983..386eccf 100644 --- a/AsBuiltReport.Microsoft.AD.psd1 +++ b/AsBuiltReport.Microsoft.AD.psd1 @@ -66,7 +66,7 @@ }, @{ ModuleName = 'Diagrammer.Microsoft.AD'; - ModuleVersion = '0.1.6' + ModuleVersion = '0.1.7' } ) From 34bb31a362bbdf4f33902d81f5687fcca6762251 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 17 Feb 2024 00:46:58 -0400 Subject: [PATCH 03/17] Improved script logging --- Src/Private/Get-AbrADCACryptographyConfig.ps1 | 1 - Src/Private/Get-AbrADCARoot.ps1 | 1 - Src/Private/Get-AbrADCASecurity.ps1 | 3 - Src/Private/Get-AbrADCASubordinate.ps1 | 2 - Src/Private/Get-AbrADCASummary.ps1 | 1 - Src/Private/Get-AbrADCATemplate.ps1 | 2 - Src/Private/Get-AbrADDCDiag.ps1 | 4 +- Src/Private/Get-AbrADDCRoleFeature.ps1 | 3 +- Src/Private/Get-AbrADDFSHealth.ps1 | 13 +- Src/Private/Get-AbrADDomain.ps1 | 4 +- Src/Private/Get-AbrADDomainController.ps1 | 199 ++++--- Src/Private/Get-AbrADDomainLastBackup.ps1 | 4 +- Src/Private/Get-AbrADDomainObject.ps1 | 16 +- Src/Private/Get-AbrADDuplicateObject.ps1 | 4 +- Src/Private/Get-AbrADDuplicateSPN.ps1 | 4 +- Src/Private/Get-AbrADExchange.ps1 | 1 - Src/Private/Get-AbrADFSMO.ps1 | 3 +- Src/Private/Get-AbrADForest.ps1 | 13 +- Src/Private/Get-AbrADGPO.ps1 | 19 +- .../Get-AbrADInfrastructureService.ps1 | 4 +- Src/Private/Get-AbrADKerberosAudit.ps1 | 8 +- Src/Private/Get-AbrADOU.ps1 | 6 +- Src/Private/Get-AbrADSecurityAssessment.ps1 | 10 +- Src/Private/Get-AbrADSite.ps1 | 553 +++++++++++++----- Src/Private/Get-AbrADSiteReplication.ps1 | 6 +- Src/Private/Get-AbrADTrust.ps1 | 1 - Src/Private/Get-AbrDNSSection.ps1 | 3 +- Src/Private/Get-AbrDomainSection.ps1 | 14 +- Src/Private/Get-AbrForestSection.ps1 | 2 +- Src/Private/Get-AbrPKISection.ps1 | 3 +- 30 files changed, 557 insertions(+), 350 deletions(-) diff --git a/Src/Private/Get-AbrADCACryptographyConfig.ps1 b/Src/Private/Get-AbrADCACryptographyConfig.ps1 index bbb6efb..5e70119 100644 --- a/Src/Private/Get-AbrADCACryptographyConfig.ps1 +++ b/Src/Private/Get-AbrADCACryptographyConfig.ps1 @@ -35,7 +35,6 @@ function Get-AbrADCACryptographyConfig { BlankLine $OutObj = @() try { - Write-PScriboMessage "Discovered Cryptography Configuration information from $($CryptoConfig.Name)." $inObj = [ordered] @{ 'CA Name' = $CryptoConfig.Name 'Server Name' = $CryptoConfig.ComputerName.ToString().ToUpper().Split(".")[0] diff --git a/Src/Private/Get-AbrADCARoot.ps1 b/Src/Private/Get-AbrADCARoot.ps1 index f1871bb..9764ab7 100644 --- a/Src/Private/Get-AbrADCARoot.ps1 +++ b/Src/Private/Get-AbrADCARoot.ps1 @@ -30,7 +30,6 @@ function Get-AbrADCARoot { BlankLine $OutObj = @() foreach ($CA in ($CAs | Where-Object { $_.IsRoot -like 'True' })) { - Write-PScriboMessage "Collecting Enterprise Root Certificate Authority information from $($CA.DisplayName)." $inObj = [ordered] @{ 'CA Name' = $CA.DisplayName 'Server Name' = $CA.ComputerName.ToString().ToUpper().Split(".")[0] diff --git a/Src/Private/Get-AbrADCASecurity.ps1 b/Src/Private/Get-AbrADCASecurity.ps1 index 1be3b9e..04a929c 100644 --- a/Src/Private/Get-AbrADCASecurity.ps1 +++ b/Src/Private/Get-AbrADCASecurity.ps1 @@ -36,7 +36,6 @@ function Get-AbrADCASecurity { BlankLine $OutObj = @() try { - Write-PScriboMessage "Collecting Certificate Validity Period information of $($CFP.Name)." $inObj = [ordered] @{ 'CA Name' = $CFP.Name 'Server Name' = $CFP.ComputerName @@ -67,7 +66,6 @@ function Get-AbrADCASecurity { Section -Style Heading4 "Access Control List (ACL)" { $OutObj = @() try { - Write-PScriboMessage "Collecting Certification Authority Access Control List information of $($CA.Name)." foreach ($ACL in $ACLs) { try { $inObj = [ordered] @{ @@ -96,7 +94,6 @@ function Get-AbrADCASecurity { try { Section -Style Heading5 "Access Rights" { $OutObj = @() - Write-PScriboMessage "Collecting AD Certification Authority Access Control List information of $($CA.Name)." foreach ($ACL in $ACLs.Access) { try { $inObj = [ordered] @{ diff --git a/Src/Private/Get-AbrADCASubordinate.ps1 b/Src/Private/Get-AbrADCASubordinate.ps1 index ac4e5e9..7e97be7 100644 --- a/Src/Private/Get-AbrADCASubordinate.ps1 +++ b/Src/Private/Get-AbrADCASubordinate.ps1 @@ -24,7 +24,6 @@ function Get-AbrADCASubordinate { process { try { - Write-PScriboMessage "Discovering Active Directory CA Enterprise Subordinate information in $($ForestInfo.toUpper())." if ($CAs | Where-Object { $_.IsRoot -like 'False' }) { Section -Style Heading2 "Enterprise Subordinate Certificate Authority" { Paragraph "The following section provides the Enterprise Subordinate CA information." @@ -32,7 +31,6 @@ function Get-AbrADCASubordinate { $OutObj = @() foreach ($CA in ($CAs | Where-Object { $_.IsRoot -like 'False' })) { try { - Write-PScriboMessage "Collecting Enterprise Subordinate Certificate Authority information from $($CA.DisplayName)." $inObj = [ordered] @{ 'CA Name' = $CA.DisplayName 'Server Name' = $CA.ComputerName.ToString().ToUpper().Split(".")[0] diff --git a/Src/Private/Get-AbrADCASummary.ps1 b/Src/Private/Get-AbrADCASummary.ps1 index 6b80ebd..a68c8db 100644 --- a/Src/Private/Get-AbrADCASummary.ps1 +++ b/Src/Private/Get-AbrADCASummary.ps1 @@ -27,7 +27,6 @@ function Get-AbrADCASummary { if ($ForestInfo) { foreach ($CA in $CAs) { try { - Write-PScriboMessage "Collecting AD Certification Authority Summary information of $($CA.DisplayName)." $inObj = [ordered] @{ 'CA Name' = $CA.DisplayName 'Server Name' = $CA.ComputerName.ToString().ToUpper().Split(".")[0] diff --git a/Src/Private/Get-AbrADCATemplate.ps1 b/Src/Private/Get-AbrADCATemplate.ps1 index 0b45cc5..3d80573 100644 --- a/Src/Private/Get-AbrADCATemplate.ps1 +++ b/Src/Private/Get-AbrADCATemplate.ps1 @@ -35,7 +35,6 @@ function Get-AbrADCATemplate { BlankLine $OutObj = @() foreach ($Template in $Templates) { - Write-PScriboMessage "Collecting $($Template.DisplayName) Issued Certificate Template information from $($CA.Name)." try { $inObj = [ordered] @{ 'Template Name' = $Template.DisplayName @@ -112,7 +111,6 @@ function Get-AbrADCATemplate { $OutObj = @() foreach ($Template in $Templates) { try { - Write-PScriboMessage "Collecting $($Template.DisplayName) Certificate Template In Active Directory." $inObj = [ordered] @{ 'Template Name' = $Template.DisplayName 'Schema Version' = $Template.SchemaVersion diff --git a/Src/Private/Get-AbrADDCDiag.ps1 b/Src/Private/Get-AbrADDCDiag.ps1 index 0f11e81..7f25f4e 100644 --- a/Src/Private/Get-AbrADDCDiag.ps1 +++ b/Src/Private/Get-AbrADDCDiag.ps1 @@ -26,7 +26,7 @@ function Get-AbrADDCDiag { ) begin { - Write-PScriboMessage "Discovering Active Directory DCDiag information for domain $Domain." + Write-PScriboMessage "Collecting Active Directory $DC DCDiag information for domain $Domain." } process { @@ -60,10 +60,8 @@ function Get-AbrADDCDiag { 'CheckSecurityError' = 'Reports on the overall health of replication with respect to Active Directory security in domain controllers running Windows Server 2003 SP1.', 'Medium' 'FrsSysVol' = 'Checks that the file replication system (FRS) system volume (SYSVOL) is ready', 'Medium' } - Write-PScriboMessage "Discovered Active Directory DCDiag information for DC $DC." foreach ($Result in $DCDIAG | Where-Object { $_.Entity -eq $($DC.ToString().split('.')[0].ToUpper()) }) { try { - Write-PScriboMessage "Collecting Active Directory DCDiag test '$($Result.TestName)' for DC $DC." $inObj = [ordered] @{ 'Test Name' = $Result.TestName 'Result' = $TextInfo.ToTitleCase($Result.TestResult) diff --git a/Src/Private/Get-AbrADDCRoleFeature.ps1 b/Src/Private/Get-AbrADDCRoleFeature.ps1 index 73a500c..d32319b 100644 --- a/Src/Private/Get-AbrADDCRoleFeature.ps1 +++ b/Src/Private/Get-AbrADDCRoleFeature.ps1 @@ -24,7 +24,7 @@ function Get-AbrADDCRoleFeature { ) begin { - Write-PScriboMessage "Discovering Active Directory DC Role & Features information of $DC." + Write-PScriboMessage "Collecting Active Directory DC Role & Features information of $DC." } process { @@ -37,7 +37,6 @@ function Get-AbrADDCRoleFeature { Remove-PSSession -Session $DCPssSession foreach ($Feature in $Features) { try { - Write-PScriboMessage "Collecting DC Roles: $($Feature.DisplayName) on $DC." $inObj = [ordered] @{ 'Name' = $Feature.DisplayName 'Parent' = $Feature.FeatureType diff --git a/Src/Private/Get-AbrADDFSHealth.ps1 b/Src/Private/Get-AbrADDFSHealth.ps1 index a26e0e8..82f6d53 100644 --- a/Src/Private/Get-AbrADDFSHealth.ps1 +++ b/Src/Private/Get-AbrADDFSHealth.ps1 @@ -24,7 +24,7 @@ function Get-AbrADDFSHealth { ) begin { - Write-PScriboMessage "Discovering AD Domain DFS Health information on $Domain." + Write-PScriboMessage "Collecting AD Domain DFS Health information on $Domain." } process { @@ -33,7 +33,6 @@ function Get-AbrADDFSHealth { if ($Options.Exclude.DCs) { $DFS = Get-WinADDFSHealth -Domain $Domain -Credential $Credential | Where-Object { $_.DomainController -notin ($Options.Exclude.DCs).split(".", 2)[0] } } Else { $DFS = Get-WinADDFSHealth -Domain $Domain -Credential $Credential } - Write-PScriboMessage "Discovered AD Domain DFS Health information from $Domain." if ($DFS) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Sysvol Replication Status' { Paragraph "The following section details the sysvol folder replication status for Domain $($Domain.ToString().ToUpper())." @@ -43,7 +42,11 @@ function Get-AbrADDFSHealth { try { $inObj = [ordered] @{ 'DC Name' = $DCStatus.DomainController - 'Replication Status' = $DCStatus.ReplicationState + 'Replication Status' = Switch ([string]::IsNullOrEmpty($DCStatus.ReplicationState)) { + $true {"Unknown"} + $false {$DCStatus.ReplicationState} + default {"--"} + } 'GPO Count' = $DCStatus.GroupPolicyCount 'Sysvol Count' = $DCStatus.SysvolCount 'Identical Count' = ConvertTo-TextYN $DCStatus.IdenticalCount @@ -100,7 +103,6 @@ function Get-AbrADDFSHealth { Write-PScriboMessage -IsWarning "Sysvol Replication Status Table Section: $($_.Exception.Message)" } 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 -Name 'DomainSysvolHealth' # Code taken from ClaudioMerola (https://github.com/ClaudioMerola/ADxRay) @@ -115,7 +117,6 @@ function Get-AbrADDFSHealth { Paragraph "The following section details domain $($Domain.ToString().ToUpper()) sysvol health status." BlankLine $OutObj = @() - Write-PScriboMessage "Collecting Sysvol information from $($Domain)." foreach ($Extension in $SYSVOLFolder) { try { $inObj = [ordered] @{ @@ -162,7 +163,6 @@ function Get-AbrADDFSHealth { Write-PScriboMessage -IsWarning "Sysvol Health Table Section: $($_.Exception.Message)" } 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 -Name 'NetlogonHealth' # Code taken from ClaudioMerola (https://github.com/ClaudioMerola/ADxRay) @@ -177,7 +177,6 @@ function Get-AbrADDFSHealth { Paragraph "The following section details domain $($Domain.ToString().ToUpper()) netlogon health status." BlankLine $OutObj = @() - Write-PScriboMessage "Collecting Netlogon information from $($Domain)." foreach ($Extension in $NetlogonFolder) { try { $inObj = [ordered] @{ diff --git a/Src/Private/Get-AbrADDomain.ps1 b/Src/Private/Get-AbrADDomain.ps1 index f34d16d..b335cce 100644 --- a/Src/Private/Get-AbrADDomain.ps1 +++ b/Src/Private/Get-AbrADDomain.ps1 @@ -24,7 +24,7 @@ function Get-AbrADDomain { ) begin { - Write-PScriboMessage "Discovering AD Domain information on forest $Forestinfo." + Write-PScriboMessage "Collecting AD Domain information on forest $Forestinfo." } process { @@ -39,9 +39,7 @@ function Get-AbrADDomain { [int64] $TEMP = $CompleteSIDS * ([math]::Pow(2, 32)) $RIDsIssued = [int32]($($RIDavailable) - $TEMP) $RIDsRemaining = $CompleteSIDS - $RIDsIssued - Write-PScriboMessage "Discovered Active Directory Domain information of domain $Domain." if ($DomainInfo) { - Write-PScriboMessage "Collecting Domain information of '$($DomainInfo)'." $inObj = [ordered] @{ 'Domain Name' = $DomainInfo.Name 'NetBIOS Name' = $DomainInfo.NetBIOSName diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index c32d3a1..7630dd8 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -32,16 +32,13 @@ function Get-AbrADDomainController { if ($InfoLevel.Domain -eq 1) { try { $OutObj = @() - Write-PScriboMessage "Discovering Active Directory Domain Controller information from $Domain." foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { $DCInfo = Invoke-Command -Session $TempPssSession { Get-ADDomainController -Identity $using:DC -Server $using:DC } - $DCComputerObject = try { Invoke-Command -Session $TempPssSession { Get-ADComputer ($using:DCInfo).ComputerObjectDN -Properties * -Server $using:DC } } catch { Out-Null } $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DCNetSettings' $DCNetSettings = try { Invoke-Command -Session $DCPssSession { Get-NetIPAddress } } catch { Out-Null } Remove-PSSession -Session $DCPssSession try { - Write-PScriboMessage "Collecting AD Domain Controllers information of $DC." $inObj = [ordered] @{ 'DC Name' = $DC.ToString().ToUpper().Split(".")[0] 'Domain Name' = Switch ([string]::IsNullOrEmpty($DCInfo.Domain)) { @@ -66,6 +63,21 @@ function Get-AbrADDomainController { } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Item)" } + } else { + try { + Write-PScriboMessage "Unable to collect infromation from $DC." + $inObj = [ordered] @{ + 'DC Name' = $DC.ToString().ToUpper().Split(".")[0] + 'Domain Name' = "Unable to Connect" + 'Site' = "--" + 'Global Catalog' = "--" + 'Read Only' = "--" + 'IP Address' = "--" + } + $OutObj += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Item)" + } } } if ($HealthCheck.DomainController.BestPractice) { @@ -97,7 +109,6 @@ function Get-AbrADDomainController { } else { try { $OutObj = @() - Write-PScriboMessage "Discovering Active Directory Domain Controller information from $Domain." foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { $DCInfo = Invoke-Command -Session $TempPssSession { Get-ADDomainController -Identity $using:DC -Server $using:DC } @@ -107,7 +118,6 @@ function Get-AbrADDomainController { Remove-PSSession -Session $DCPssSession if ($InfoLevel.Domain -eq 1) { try { - Write-PScriboMessage "Collecting AD Domain Controllers information of $DC." $inObj = [ordered] @{ 'DC Name' = $DC.ToString().ToUpper().Split(".")[0] 'Domain Name' = Switch ([string]::IsNullOrEmpty($DCInfo.Domain)) { @@ -158,95 +168,106 @@ function Get-AbrADDomainController { } else { try { Section -Style Heading4 $DCInfo.Name { - Write-PScriboMessage "Collecting AD Domain Controllers information of $DC." - Section -ExcludeFromTOC -Style NOTOCHeading5 "General Information" { - $inObj = [ordered] @{ - 'DC Name' = $DCInfo.Hostname - 'Domain Name' = Switch ([string]::IsNullOrEmpty($DCInfo.Domain)) { - $true { "--" } - $false { $DCInfo.Domain } - default { "Unknown" } - } - 'Site' = Switch ([string]::IsNullOrEmpty($DCInfo.Site)) { - $true { "--" } - $false { $DCInfo.Site } - default { "Unknown" } + try { + Section -ExcludeFromTOC -Style NOTOCHeading5 "General Information" { + $inObj = [ordered] @{ + 'DC Name' = $DCInfo.Hostname + 'Domain Name' = Switch ([string]::IsNullOrEmpty($DCInfo.Domain)) { + $true { "--" } + $false { $DCInfo.Domain } + default { "Unknown" } + } + 'Site' = Switch ([string]::IsNullOrEmpty($DCInfo.Site)) { + $true { "--" } + $false { $DCInfo.Site } + default { "Unknown" } + } + 'Global Catalog' = ConvertTo-TextYN $DCInfo.IsGlobalCatalog + 'Read Only' = ConvertTo-TextYN $DCInfo.IsReadOnly + 'Location' = $DCComputerObject.Location + 'Computer Object SID' = $DCComputerObject.SID + "Operating System" = $DCInfo.OperatingSystem + 'Description' = $DCComputerObject.Description } - 'Global Catalog' = ConvertTo-TextYN $DCInfo.IsGlobalCatalog - 'Read Only' = ConvertTo-TextYN $DCInfo.IsReadOnly - 'Location' = $DCComputerObject.Location - 'Computer Object SID' = $DCComputerObject.SID - "Operating System" = $DCInfo.OperatingSystem - 'Description' = $DCComputerObject.Description - } - $OutObj = [pscustomobject]$inobj + $OutObj = [pscustomobject]$inobj - $TableParams = @{ - Name = "General Information - $($DCInfo.Name)" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" + $TableParams = @{ + Name = "General Information - $($DCInfo.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Table @TableParams } - $OutObj | Table @TableParams + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (General Information Section)" } - Section -ExcludeFromTOC -Style NOTOCHeading5 "Partitions" { - $inObj = [ordered] @{ - 'Default Partition' = $DCInfo.DefaultPartition - 'Partitions' = $DCInfo.Partitions - } - $OutObj = [pscustomobject]$inobj + try { + Section -ExcludeFromTOC -Style NOTOCHeading5 "Partitions" { + $inObj = [ordered] @{ + 'Default Partition' = $DCInfo.DefaultPartition + 'Partitions' = $DCInfo.Partitions + } + $OutObj = [pscustomobject]$inobj - $TableParams = @{ - Name = "Partitions - $($DCInfo.Name)" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - $OutObj | Table @TableParams - } - Section -ExcludeFromTOC -Style NOTOCHeading5 "Networking Settings" { - $inObj = [ordered] @{ - 'IPv4 Addresses' = Switch ([string]::IsNullOrEmpty((($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address))) { - $true { "--" } - $false { ($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address -join "," } - default { "Unknown" } + $TableParams = @{ + Name = "Partitions - $($DCInfo.Name)" + List = $true + ColumnWidths = 40, 60 } - 'IPv6 Addresses' = Switch ([string]::IsNullOrEmpty((($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -ne '::1' }).IPv6Address))) { - $true { "--" } - $false { ($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -ne '::1' }).IPv6Address -join "," } - default { "Unknown" } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" } - "LDAP Port" = $DCInfo.LdapPort - "SSL Port" = $DCInfo.SslPort + $OutObj | Table @TableParams } - $OutObj = [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Partitions Section)" + } + try { + Section -ExcludeFromTOC -Style NOTOCHeading5 "Networking Settings" { + $inObj = [ordered] @{ + 'IPv4 Addresses' = Switch ([string]::IsNullOrEmpty((($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address))) { + $true { "--" } + $false { ($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address -join "," } + default { "Unknown" } + } + 'IPv6 Addresses' = Switch ([string]::IsNullOrEmpty((($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -ne '::1' }).IPv6Address))) { + $true { "--" } + $false { ($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -ne '::1' }).IPv6Address -join "," } + default { "Unknown" } + } + "LDAP Port" = $DCInfo.LdapPort + "SSL Port" = $DCInfo.SslPort + } + $OutObj = [pscustomobject]$inobj - if ($HealthCheck.DomainController.BestPractice) { - $OutObj | Where-Object { $_.'IPv4 Addresses'.Split(",").Count -gt 1 } | Set-Style -Style Warning -Property 'IPv4 Addresses' - } + if ($HealthCheck.DomainController.BestPractice) { + $OutObj | Where-Object { $_.'IPv4 Addresses'.Split(",").Count -gt 1 } | Set-Style -Style Warning -Property 'IPv4 Addresses' + } - $TableParams = @{ - Name = "Networking Settings - $($DCInfo.Name)" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - $OutObj | Table @TableParams - if ($HealthCheck.DomainController.BestPractice -and ($OutObj | Where-Object { $_.'IPv4 Addresses'.Split(",").Count -gt 1 })) { - Paragraph "Health Check:" -Bold -Underline - BlankLine - Paragraph { - Text "Best Practice:" -Bold - Text "On Domain Controllers with more than one NIC where each NIC is connected to separate Network, there's a possibility that the Host A DNS registration can occur for unwanted NICs. Avoid registering unwanted NICs in DNS on a multihomed domain controller" + $TableParams = @{ + Name = "Networking Settings - $($DCInfo.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Table @TableParams + if ($HealthCheck.DomainController.BestPractice -and ($OutObj | Where-Object { $_.'IPv4 Addresses'.Split(",").Count -gt 1 })) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "On Domain Controllers with more than one NIC where each NIC is connected to separate Network, there's a possibility that the Host A DNS registration can occur for unwanted NICs. Avoid registering unwanted NICs in DNS on a multihomed domain controller" + } } } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Networking Settings Section)" } try { Section -ExcludeFromTOC -Style NOTOCHeading5 'Hardware Inventory' { @@ -334,7 +355,6 @@ function Get-AbrADDomainController { $OutObj = @() 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 -Name 'DNSIPConfiguration' try { $DCIPAddress = Invoke-Command -Session $DCPssSession { [System.Net.Dns]::GetHostAddresses($using:DC).IPAddressToString } @@ -427,13 +447,11 @@ function Get-AbrADDomainController { } try { - Write-PScriboMessage "Collecting AD Domain Controller NTDS information." Section -Style Heading4 'NTDS Information' { $OutObj = @() foreach ($DC in $DCs) { 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 -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 } @@ -470,13 +488,11 @@ function Get-AbrADDomainController { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (NTDS Table)" } try { - Write-PScriboMessage "Collecting AD Domain Controller Time Source information." Section -Style Heading4 'Time Source Information' { $OutObj = @() foreach ($DC in $DCs) { 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 -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' } @@ -523,14 +539,11 @@ function Get-AbrADDomainController { } if ($HealthCheck.DomainController.Diagnostic) { try { - Write-PScriboMessage "Collecting AD Domain Controller SRV Records Status." Section -Style Heading4 'SRV Records Status' { $OutObj = @() - Write-PScriboMessage "Discovering Active Directory Domain Controller SRV Records Status in $Domain." foreach ($DC in $DCs) { 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 -Name 'SRVRecordsStatus' $PDCEmulator = Invoke-Command -Session $TempPssSession { (Get-ADDomain $using:Domain -ErrorAction Stop).PDCEmulator } if ($Domain -eq $ADSystem.RootDomain) { @@ -635,11 +648,9 @@ function Get-AbrADDomainController { } 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) { @@ -697,13 +708,11 @@ function Get-AbrADDomainController { } if ($HealthCheck.DomainController.Software) { try { - Write-PScriboMessage "Collecting additional software running on the Domain Controller." $DCObj = @() $DCObj += foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { $Software = @() - Write-PScriboMessage "Collecting AD Domain Controller installed software information for $DC." $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 } @@ -767,16 +776,14 @@ function Get-AbrADDomainController { } } } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Section)" + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Installed Software Section)" } try { $DCObj = @() $DCObj += foreach ($DC in $DCs) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { - Write-PScriboMessage "Collecting pending/missing patch information from Domain Controller $($DC)." try { $Software = @() - 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 diff --git a/Src/Private/Get-AbrADDomainLastBackup.ps1 b/Src/Private/Get-AbrADDomainLastBackup.ps1 index 245b7ca..a64b0a9 100644 --- a/Src/Private/Get-AbrADDomainLastBackup.ps1 +++ b/Src/Private/Get-AbrADDomainLastBackup.ps1 @@ -24,20 +24,18 @@ function Get-AbrADDomainLastBackup { ) begin { - Write-PScriboMessage "Discovering AD Domain last backup information on $Domain." + Write-PScriboMessage "Collecting AD Domain last backup information on $Domain." } process { if ($Domain -and $HealthCheck.Domain.Backup) { try { $LastBackups = Get-WinADLastBackup -Domain $Domain -Credential $Credential - Write-PScriboMessage "Discovered last taken backup information of domain $Domain." if ($LastBackups) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Naming Context Last Backup' { Paragraph "The following section details naming context last backup time for Domain $($Domain.ToString().ToUpper())." BlankLine $OutObj = @() - Write-PScriboMessage "Collecting Naming Context Last Backup information of $($Domain)." foreach ($LastBackup in $LastBackups) { try { $inObj = [ordered] @{ diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index ef0c405..e91797c 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -24,14 +24,13 @@ function Get-AbrADDomainObject { ) begin { - Write-PScriboMessage "Discovering AD Domain Objects information on forest $Forestinfo." + Write-PScriboMessage "Collecting AD Domain Objects information on forest $Forestinfo." } process { try { Section -Style Heading3 'Domain Object Stats' { if ($Domain) { - Write-PScriboMessage "Collecting the Active Directory Object Count of domain $Domain." try { $ADLimitedProperties = @("Name", "Enabled", "SAMAccountname", "DisplayName", "Enabled", "LastLogonDate", "PasswordLastSet", "PasswordNeverExpires", "PasswordNotRequired", "PasswordExpired", "SmartcardLogonRequired", "AccountExpirationDate", "AdminCount", "Created", "Modified", "LastBadPasswordAttempt", "badpwdcount", "mail", "CanonicalName", "DistinguishedName", "ServicePrincipalName", "SIDHistory", "PrimaryGroupID", "UserAccountControl", "CannotChangePassword", "PwdLastSet", "LockedOut", "TrustedForDelegation", "TrustedtoAuthForDelegation", "msds-keyversionnumber", "SID", "AccountNotDelegated", "EmailAddress") $script:DC = Invoke-Command -Session $TempPssSession { (Get-ADDomain -Identity $using:Domain).ReplicaDirectoryServers | Select-Object -First 1 } @@ -181,7 +180,6 @@ function Get-AbrADDomainObject { $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." foreach ($Category in $Categories) { try { if ($Category -eq 'Total Users') { @@ -272,7 +270,6 @@ function Get-AbrADDomainObject { Section -Style Heading3 'Privileged Groups' { $OutObj = @() if ($Domain) { - Write-PScriboMessage "Collecting Privileged Group in Active Directory." try { $DomainSID = Invoke-Command -Session $TempPssSession { (Get-ADDomain -Identity $using:Domain).domainsid.Value } if ($Domain -eq $ADSystem.Name) { @@ -288,7 +285,6 @@ function Get-AbrADDomainObject { try { $Group = $GroupOBj | Where-Object { $_.SID -like $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 @@ -355,7 +351,6 @@ function Get-AbrADDomainObject { try { $Group = $GroupOBj | Where-Object { $_.SID -like $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, LastLogonDate, passwordNeverExpires, Enabled -SearchBase (Get-ADDomain -Identity $using:Domain).distinguishedName } } if ($GroupObjects) { Section -ExcludeFromTOC -Style NOTOCHeading4 "$($Group.Name) ($(($GroupObjects | Measure-Object).count) Members)" { @@ -462,7 +457,6 @@ function Get-AbrADDomainObject { $SidHistory = $Computers.SIDHistory $Categories = @('Total Computers', 'Dormant (> 90 days)', 'Password Age (> 30 days)', 'SidHistory') if ($Categories) { - Write-PScriboMessage "Collecting Status of Computer Accounts." foreach ($Category in $Categories) { try { if ($Category -eq 'Total Computers') { @@ -537,7 +531,6 @@ function Get-AbrADDomainObject { Section -Style Heading3 'Operating Systems Count' { $OutObj = @() if ($Domain) { - Write-PScriboMessage "Collecting Operating Systems in Active Directory." try { $OSObjects = $Computers | Where-Object { $_.name -like '*' } | Group-Object -Property operatingSystem | Select-Object Name, Count if ($OSObjects) { @@ -585,7 +578,6 @@ function Get-AbrADDomainObject { Section -Style Heading3 'Default Domain Password Policy' { $OutObj = @() if ($Domain) { - Write-PScriboMessage "Collecting the Active Directory Default Domain Password Policy of domain $Item." try { $PasswordPolicy = Invoke-Command -Session $TempPssSession { Get-ADDefaultDomainPasswordPolicy -Identity $using:Domain } if ($PasswordPolicy) { @@ -624,7 +616,6 @@ function Get-AbrADDomainObject { 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 if ($PasswordPolicy) { @@ -695,7 +686,6 @@ function Get-AbrADDomainObject { 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 @@ -764,9 +754,7 @@ function Get-AbrADDomainObject { try { if ($Domain) { - 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 * } if ($GMSA) { Section -Style Heading3 'gMSA Identities' { @@ -900,9 +888,7 @@ function Get-AbrADDomainObject { } try { if ($Domain) { - Write-PScriboMessage "Collecting the Active Directory Foreign Security Principals." try { - Write-PScriboMessage "Collecting the Active Directory Foreign Security Principals from DC $DC." $FSP = Invoke-Command -Session $TempPssSession { Get-ADObject -Server $using:DC -Filter { ObjectClass -eq "foreignSecurityPrincipal" } -Properties msds-principalname, memberof } if ($FSP) { Section -Style Heading3 'Foreign Security Principals' { diff --git a/Src/Private/Get-AbrADDuplicateObject.ps1 b/Src/Private/Get-AbrADDuplicateObject.ps1 index 0b99c8c..ca5df63 100644 --- a/Src/Private/Get-AbrADDuplicateObject.ps1 +++ b/Src/Private/Get-AbrADDuplicateObject.ps1 @@ -24,14 +24,13 @@ function Get-AbrADDuplicateObject { ) begin { - Write-PScriboMessage "Discovering duplicate Objects information on $Domain." + Write-PScriboMessage "Collecting duplicate Objects information on $Domain." } process { if ($HealthCheck.Domain.DuplicateObject) { try { $Objects = Get-WinADDuplicateObject -Domain $Domain -Credential $Credential - Write-PScriboMessage "Discovered AD Duplicate Objects information from $Domain." if ($Objects) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Duplicate Objects' { Paragraph "The following section details Duplicate Objects discovered on Domain $($Domain.ToString().ToUpper())." @@ -39,7 +38,6 @@ function Get-AbrADDuplicateObject { $OutObj = @() foreach ($Object in $Objects) { try { - Write-PScriboMessage "Collecting $($Object.Name) information from $($Domain)." $inObj = [ordered] @{ 'Name' = $Object.Name 'Created' = $Object.WhenCreated.ToString("yyyy:MM:dd") diff --git a/Src/Private/Get-AbrADDuplicateSPN.ps1 b/Src/Private/Get-AbrADDuplicateSPN.ps1 index c24f256..a4039f5 100644 --- a/Src/Private/Get-AbrADDuplicateSPN.ps1 +++ b/Src/Private/Get-AbrADDuplicateSPN.ps1 @@ -24,14 +24,13 @@ function Get-AbrADDuplicateSPN { ) begin { - Write-PScriboMessage "Discovering duplicate SPN information on $Domain." + Write-PScriboMessage "Collecting duplicate SPN information on $Domain." } process { if ($HealthCheck.Domain.SPN) { try { $SPNs = Get-WinADDuplicateSPN -Domain $Domain -Credential $Credential - Write-PScriboMessage "Discovered AD Duplicate SPN information from $Domain." if ($SPNs) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Duplicate SPN' { Paragraph "The following section details Duplicate SPN discovered on Domain $($Domain.ToString().ToUpper())." @@ -39,7 +38,6 @@ function Get-AbrADDuplicateSPN { $OutObj = @() foreach ($SPN in $SPNs) { try { - Write-PScriboMessage "Collecting $($SPN.Name) information from $($Domain)." $inObj = [ordered] @{ 'Name' = $SPN.Name 'Count' = $SPN.Count diff --git a/Src/Private/Get-AbrADExchange.ps1 b/Src/Private/Get-AbrADExchange.ps1 index fed2072..c6efb6c 100644 --- a/Src/Private/Get-AbrADExchange.ps1 +++ b/Src/Private/Get-AbrADExchange.ps1 @@ -32,7 +32,6 @@ function Get-AbrADExchange { $EXInfo = @() foreach ($EXServer in $EXServers) { try { - Write-PScriboMessage "Collecting Active Directory Exchange information from $($ForestInfo.toUpper())" $inObj = [ordered] @{ 'Name' = $EXServer.Name 'Dns Name' = $EXServer.DnsHostName diff --git a/Src/Private/Get-AbrADFSMO.ps1 b/Src/Private/Get-AbrADFSMO.ps1 index df4dff0..2b2dbbe 100644 --- a/Src/Private/Get-AbrADFSMO.ps1 +++ b/Src/Private/Get-AbrADFSMO.ps1 @@ -24,7 +24,7 @@ function Get-AbrADFSMO { ) begin { - Write-PScriboMessage "Discovering Active Directory FSMO information of domain $Domain." + Write-PScriboMessage "Collecting Active Directory FSMO information of domain $Domain." } process { @@ -38,7 +38,6 @@ function Get-AbrADFSMO { $IsInfraMasterGC = (Invoke-Command -Session $DCPssSession { Get-ADDomainController -Identity ($using:DomainData).InfrastructureMaster }).IsGlobalCatalog $OutObj = @() try { - Write-PScriboMessage "Discovered Active Directory FSMO information of domain $Domain." $inObj = [ordered] @{ 'Infrastructure Master' = $DomainData.InfrastructureMaster 'RID Master' = $DomainData.RIDMaster diff --git a/Src/Private/Get-AbrADForest.ps1 b/Src/Private/Get-AbrADForest.ps1 index 48f1b23..91eda9b 100644 --- a/Src/Private/Get-AbrADForest.ps1 +++ b/Src/Private/Get-AbrADForest.ps1 @@ -19,14 +19,13 @@ function Get-AbrADForest { ) begin { - Write-PScriboMessage "Discovering Active Directory forest information." + Write-PScriboMessage "Collecting Active Directory forest information." } process { try { $Data = Invoke-Command -Session $TempPssSession { Get-ADForest } $ForestInfo = $Data.RootDomain.toUpper() - Write-PScriboMessage "Discovered Active Directory information of forest $ForestInfo." $DomainDN = Invoke-Command -Session $TempPssSession { (Get-ADDomain -Identity (Get-ADForest | Select-Object -ExpandProperty RootDomain )).DistinguishedName } $TombstoneLifetime = Invoke-Command -Session $TempPssSession { Get-ADObject "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,$using:DomainDN" -Properties tombstoneLifetime | Select-Object -ExpandProperty tombstoneLifetime } $ADVersion = Invoke-Command -Session $TempPssSession { Get-ADObject (Get-ADRootDSE).schemaNamingContext -property objectVersion | Select-Object -ExpandProperty objectVersion } @@ -42,7 +41,6 @@ function Get-AbrADForest { ElseIf ($ADVersion -eq '30') { $server = 'Windows Server 2003' } $OutObj = @() if ($Data) { - Write-PScriboMessage "Collecting Active Directory information of forest $ForestInfo." foreach ($Item in $Data) { try { $inObj = [ordered] @{ @@ -140,16 +138,13 @@ function Get-AbrADForest { Paragraph "The following section provides a summary of the Active Directory PKI Infrastructure Information." BlankLine } - Write-PScriboMessage "Discovering certificate authority information on forest $ForestInfo." $ConfigNCDN = $Data.PartitionsContainer.Split(',') | Select-Object -Skip 1 $rootCA = Get-ADObjectSearch -DN "CN=Certification Authorities,CN=Public Key Services,CN=Services,$($ConfigNCDN -join ',')" -Filter { objectClass -eq "certificationAuthority" } -Properties "Name" -SelectPrty 'DistinguishedName', 'Name' -Session $TempPssSession if ($rootCA) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Certification Authority Root(s)' { $OutObj = @() - Write-PScriboMessage "Discovered Certificate Authority Information on forest $ForestInfo." foreach ($Item in $rootCA) { try { - Write-PScriboMessage "Collecting Certificate Authority Information '$($Item.Name)'" $inObj = [ordered] @{ 'Name' = $Item.Name 'Distinguished Name' = $Item.DistinguishedName @@ -185,16 +180,13 @@ function Get-AbrADForest { } else { Write-PScriboMessage -IsWarning "No Certificate Authority Root information found in $ForestInfo, disabling the section." } - Write-PScriboMessage "Discovering certificate authority issuers on forest $ForestInfo." $ConfigNCDN = $Data.PartitionsContainer.Split(',') | Select-Object -Skip 1 $subordinateCA = Get-ADObjectSearch -DN "CN=Enrollment Services,CN=Public Key Services,CN=Services,$($ConfigNCDN -join ',')" -Filter { objectClass -eq "pKIEnrollmentService" } -Properties "*" -SelectPrty 'dNSHostName', 'Name' -Session $TempPssSession if ($subordinateCA) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Certification Authority Issuer(s)' { $OutObj = @() - Write-PScriboMessage "Discovered Certificate Authority issuers on forest $ForestInfo." foreach ($Item in $subordinateCA) { try { - Write-PScriboMessage "Collecting Certificate Authority issuers '$($Item.Name)'" $inObj = [ordered] @{ 'Name' = $Item.Name 'DNS Name' = $Item.dNSHostName @@ -224,14 +216,11 @@ function Get-AbrADForest { } try { Section -Style Heading3 'Optional Features' { - Write-PScriboMessage "Discovering Optional Features enabled on forest $ForestInfo." $Data = Invoke-Command -Session $TempPssSession { Get-ADOptionalFeature -Filter * } $OutObj = @() if ($Data) { - Write-PScriboMessage "Discovered Optional Features enabled on forest $ForestInfo." foreach ($Item in $Data) { try { - Write-PScriboMessage "Collecting Optional Features '$($Item.Name)'" $inObj = [ordered] @{ 'Name' = $Item.Name 'Required Forest Mode' = $Item.RequiredForestMode diff --git a/Src/Private/Get-AbrADGPO.ps1 b/Src/Private/Get-AbrADGPO.ps1 index b22bd89..57a3eea 100644 --- a/Src/Private/Get-AbrADGPO.ps1 +++ b/Src/Private/Get-AbrADGPO.ps1 @@ -24,7 +24,7 @@ function Get-AbrADGPO { ) begin { - Write-PScriboMessage "Discovering Active Directory Group Policy Objects information for $($Domain.ToString().ToUpper())." + Write-PScriboMessage "Collecting Active Directory Group Policy Objects information for $($Domain.ToString().ToUpper())." } process { @@ -34,14 +34,12 @@ function Get-AbrADGPO { BlankLine $OutObj = @() $GPOs = Invoke-Command -Session $TempPssSession -ScriptBlock { Get-GPO -Domain $using:Domain -All } - Write-PScriboMessage "Discovered Active Directory Group Policy Objects information on $Domain. (Group Policy Objects)" if ($GPOs) { if ($InfoLevel.Domain -eq 1) { try { foreach ($GPO in $GPOs) { try { [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 'GPO Status' = ($GPO.GpoStatus -creplace '([A-Z\W_]|\d+)(?=30 days and PasswordLastSet >= 365 days) on Domain $($Domain.ToString().ToUpper())" BlankLine $OutObj = @() - Write-PScriboMessage "Collecting Inactive Privileged Accounts information from $($Domain)." foreach ($InactivePrivilegedUser in $InactivePrivilegedUsers) { try { $inObj = [ordered] @{ @@ -261,13 +255,11 @@ function Get-AbrADSecurityAssessment { } try { $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 NOTOCHeading4 'Service Accounts Assessment' { Paragraph "The following section details probable AD Service Accounts (user accounts with SPNs) on Domain $($Domain.ToString().ToUpper())" 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 { diff --git a/Src/Private/Get-AbrADSite.ps1 b/Src/Private/Get-AbrADSite.ps1 index 99c3db3..752ee59 100644 --- a/Src/Private/Get-AbrADSite.ps1 +++ b/Src/Private/Get-AbrADSite.ps1 @@ -19,78 +19,81 @@ function Get-AbrADSite { ) begin { - Write-PScriboMessage "Discovering Active Directory Sites information of forest $ForestInfo" + Write-PScriboMessage "Collecting Active Directory Sites information of forest $ForestInfo" } process { try { $Site = Invoke-Command -Session $TempPssSession { Get-ADReplicationSite -Filter * -Properties * } if ($Site) { - Section -Style Heading3 'Sites' { - $OutObj = @() - foreach ($Item in $Site) { - try { - Write-PScriboMessage "Collecting '$($Item.Name)' Site" - $SubnetArray = @() - $Subnets = $Item.Subnets - foreach ($Object in $Subnets) { - $SubnetName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSubnet $using:Object } - $SubnetArray += $SubnetName.Name - } - $inObj = [ordered] @{ - 'Site Name' = $Item.Name - 'Description' = ConvertTo-EmptyToFiller $Item.Description - 'Subnets' = Switch (($SubnetArray).count) { - 0 { "No subnet assigned" } - default { $SubnetArray } + Section -Style Heading3 'Replication' { + Paragraph "Replication is the process of transferring and updating Active Directory objects between + domain controllers in the Active Directory domain and forest. The folowing setion details Active Directory replication and it´s relationships." + BlankLine + Section -Style Heading4 'Sites' { + $OutObj = @() + foreach ($Item in $Site) { + try { + $SubnetArray = @() + $Subnets = $Item.Subnets + foreach ($Object in $Subnets) { + $SubnetName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSubnet $using:Object } + $SubnetArray += $SubnetName.Name } - 'Domain Controllers' = & { - $ServerArray = @() - $Servers = try { Get-ADObjectSearch -DN "CN=Servers,$($Item.DistinguishedName)" -Filter { objectClass -eq "Server" } -Properties "DNSHostName" -SelectPrty 'DNSHostName', 'Name' -Session $TempPssSession } catch { 'Unknown' } - foreach ($Object in $Servers) { - $ServerArray += $Object.Name + $inObj = [ordered] @{ + 'Site Name' = $Item.Name + 'Description' = ConvertTo-EmptyToFiller $Item.Description + 'Subnets' = Switch (($SubnetArray).count) { + 0 { "No subnet assigned" } + default { $SubnetArray } } + 'Domain Controllers' = & { + $ServerArray = @() + $Servers = try { Get-ADObjectSearch -DN "CN=Servers,$($Item.DistinguishedName)" -Filter { objectClass -eq "Server" } -Properties "DNSHostName" -SelectPrty 'DNSHostName', 'Name' -Session $TempPssSession } catch { 'Unknown' } + foreach ($Object in $Servers) { + $ServerArray += $Object.Name + } - if ($ServerArray) { - return $ServerArray - } else { 'No DC assigned' } + if ($ServerArray) { + return $ServerArray + } else { 'No DC assigned' } + } } - } - $OutObj += [pscustomobject]$inobj + $OutObj += [pscustomobject]$inobj - if ($HealthCheck.Site.BestPractice) { - $OutObj | Where-Object { $_.'Subnets' -eq 'No subnet assigned' } | Set-Style -Style Warning -Property 'Subnets' - $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' - $OutObj | Where-Object { $_.'Domain Controllers' -eq 'No DC assigned' } | Set-Style -Style Warning -Property 'Domain Controllers' + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Subnets' -eq 'No subnet assigned' } | Set-Style -Style Warning -Property 'Subnets' + $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Domain Controllers' -eq 'No DC assigned' } | Set-Style -Style Warning -Property 'Domain Controllers' + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Site)" } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Site)" } - } - $TableParams = @{ - Name = "Sites - $($ForestInfo)" - List = $false - ColumnWidths = 25, 30, 20, 25 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - $OutObj | Sort-Object -Property 'Site Name' | Table @TableParams - if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Subnets' -eq '--' }) -or ($OutObj | Where-Object { $_.'Description' -eq '--' }))) { - Paragraph "Health Check:" -Bold -Underline - BlankLine - if ($OutObj | Where-Object { $_.'Subnets' -eq 'No subnet assigned' }) { - Paragraph { - Write-PScriboMessage "Discovered Active Directory Sites information of forest $ForestInfo" - Text -Bold "Corrective Actions:" - Text "Ensure Sites have an associated subnet. If subnets are not associated with AD Sites users in the AD Sites might choose a remote domain controller for authentication which in turn might result in excessive use of a remote domain controller." } + $TableParams = @{ + Name = "Sites - $($ForestInfo)" + List = $false + ColumnWidths = 25, 30, 20, 25 } - if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Site Name' | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Subnets' -eq '--' }) -or ($OutObj | Where-Object { $_.'Description' -eq '--' }))) { + Paragraph "Health Check:" -Bold -Underline BlankLine - Paragraph { - Text "Best Practice:" -Bold - Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + if ($OutObj | Where-Object { $_.'Subnets' -eq 'No subnet assigned' }) { + Paragraph { + Text -Bold "Corrective Actions:" + Text "Ensure Sites have an associated subnet. If subnets are not associated with AD Sites users in the AD Sites might choose a remote domain controller for authentication which in turn might result in excessive use of a remote domain controller." } + } + if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { + BlankLine + Paragraph { + Text "Best Practice:" -Bold + Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + } } } } @@ -99,7 +102,6 @@ function Get-AbrADSite { if ($Replications) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Connection Objects' { $OutObj = @() - Write-PScriboMessage "Discovered Connection Objects information of forest $ForestInfo" foreach ($Repl in $Replications) { try { $inObj = [ordered] @{ @@ -155,10 +157,8 @@ function Get-AbrADSite { if ($Subnet) { Section -Style Heading4 'Site Subnets' { $OutObj = @() - Write-PScriboMessage "Discovered Active Directory Sites Subnets information of forest $ForestInfo" foreach ($Item in $Subnet) { try { - Write-PScriboMessage "Collecting $($Item.Name) Site Subnet." $inObj = [ordered] @{ 'Subnet' = $Item.Name 'Description' = ConvertTo-EmptyToFiller $Item.Description @@ -209,7 +209,6 @@ function Get-AbrADSite { try { $OutObj = @() foreach ($Domain in $ADSystem.Domains | Where-Object { $_ -notin $Options.Exclude.Domains }) { - Write-PScriboMessage "Discovered Missing Subnet in AD information from $Domain." $DomainInfo = Invoke-Command -Session $TempPssSession { Get-ADDomain $using:Domain -ErrorAction Stop } foreach ($DC in ($DomainInfo.ReplicaDirectoryServers | Where-Object { $_ -notin $Options.Exclude.DCs })) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { @@ -217,7 +216,6 @@ function Get-AbrADSite { $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") { @@ -293,7 +291,7 @@ function Get-AbrADSite { } if ($Graph) { - Section -Style Heading3 "Site Inventory Diagram." { + Section -Style Heading4 "Site Inventory Diagram." { Image -Base64 $Graph -Text "Site Inventory Diagram" -Percent (Get-ImagePercent -Graph $Graph) -Align Center Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 } @@ -304,91 +302,386 @@ function Get-AbrADSite { } } try { - $Link = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLink -Filter * -Properties * } - if ($Link) { - Section -Style Heading4 'Site Links' { - $OutObj = @() - Write-PScriboMessage "Discovered Active Directory Sites Link information of forest $ForestInfo" - foreach ($Item in $Link) { - try { - Write-PScriboMessage "Collecting '$($Item.Name)' Site Link" + $DomainDN = Invoke-Command -Session $TempPssSession { (Get-ADDomain -Identity (Get-ADForest | Select-Object -ExpandProperty RootDomain )).DistinguishedName } + $InterSiteTransports = Invoke-Command -Session $TempPssSession { Get-ADObject -Filter { (objectClass -eq "interSiteTransport") } -SearchBase "CN=Inter-Site Transports,CN=Sites,CN=Configuration,$using:DomainDN" -Properties * } + if ($InterSiteTransports) { + Section -Style Heading4 'Inter-Site Transports' { + Paragraph "Site links in Active Directory represent the inter-site connectivity and method used to transfer replication traffic.There are two transport protocols that can be used for replication via site links. The default protocol used in site link is IP, and it performs synchronous replication between available domain controllers. The SMTP method can be used when the link between sites is not reliable." + BlankLine + try { + $OutObj = @() + foreach ($Item in $InterSiteTransports) { $SiteArray = @() - $Sites = $Item.siteList - foreach ($Object in $Sites) { - $SiteName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSite -Identity $using:Object } - $SiteArray += $SiteName.Name - } - $inObj = [ordered] @{ - 'Site Link Name' = $Item.Name - 'Cost' = $Item.Cost - 'Replication Frequency' = "$($Item.ReplicationFrequencyInMinutes) min" - 'Transport Protocol' = $Item.InterSiteTransportProtocol - 'Options' = Switch ($Item.Options) { - $null { 'Change Notification is Disabled' } - '0' { '(0)Change Notification is Disabled' } - '1' { '(1)Change Notification is Enabled with Compression' } - '2' { '(2)Force sync in opposite direction at end of sync' } - '3' { '(3)Change Notification is Enabled with Compression and Force sync in opposite direction at end of sync' } - '4' { '(4)Disable compression of Change Notification messages' } - '5' { '(5)Change Notification is Enabled without Compression' } - '6' { '(6)Force sync in opposite direction at end of sync and Disable compression of Change Notification messages' } - '7' { '(7)Change Notification is Enabled without Compression and Force sync in opposite direction at end of sync' } - Default { "Unknown siteLink option: $($Item.Options)" } + Switch ($Item.options) { + $null { + $BridgeAlSiteLinks = "Yes" + $IgnoreSchedules = "No" + } + 0 { + $BridgeAlSiteLinks = "Yes" + $IgnoreSchedules = "No" + } + 1 { + $BridgeAlSiteLinks = "Yes" + $IgnoreSchedules = "Yes" + } + 2 { + $BridgeAlSiteLinks = "No" + $IgnoreSchedules = "No" + } + 3 { + $BridgeAlSiteLinks = "No" + $IgnoreSchedules = "Yes" + } + default { + $BridgeAlSiteLinks = "Unknown" + $IgnoreSchedules = "Unknown" } - 'Sites' = $SiteArray -join "; " - 'Protected From Accidental Deletion' = ConvertTo-TextYN $Item.ProtectedFromAccidentalDeletion - 'Description' = ConvertTo-EmptyToFiller $Item.Description } - $OutObj = [pscustomobject]$inobj - if ($HealthCheck.Site.BestPractice) { - $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' - $OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' } | Set-Style -Style Warning -Property 'Options' - $OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' } | Set-Style -Style Warning -Property 'Protected From Accidental Deletion' + $inObj = [ordered] @{ + 'Name' = $Item.Name + 'Bridge All Site Links' = $BridgeAlSiteLinks + 'Ignore Schedules' = $IgnoreSchedules } + $OutObj += [pscustomobject]$inobj + } - $TableParams = @{ - Name = "Site Links - $($Item.Name)" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" + $TableParams = @{ + Name = "Inter-Site Transports - $($ForestInfo)" + List = $false + ColumnWidths = 34, 33, 33 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Inter-Site Transports section)" + } + try { + Section -Style Heading4 'IP' { + try { + $IPLink = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLink -Filter * -Properties * | Where-Object { $_.InterSiteTransportProtocol -eq "IP" } } + if ($IPLink) { + Section -Style Heading5 'Site Links' { + $OutObj = @() + foreach ($Item in $IPLink) { + try { + $SiteArray = @() + $Sites = $Item.siteList + foreach ($Object in $Sites) { + $SiteName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSite -Identity $using:Object } + $SiteArray += $SiteName.Name + } + $inObj = [ordered] @{ + 'Site Link Name' = $Item.Name + 'Cost' = $Item.Cost + 'Replication Frequency' = "$($Item.ReplicationFrequencyInMinutes) min" + 'Transport Protocol' = $Item.InterSiteTransportProtocol + 'Options' = Switch ($Item.Options) { + $null { 'Change Notification is Disabled' } + '0' { '(0)Change Notification is Disabled' } + '1' { '(1)Change Notification is Enabled with Compression' } + '2' { '(2)Force sync in opposite direction at end of sync' } + '3' { '(3)Change Notification is Enabled with Compression and Force sync in opposite direction at end of sync' } + '4' { '(4)Disable compression of Change Notification messages' } + '5' { '(5)Change Notification is Enabled without Compression' } + '6' { '(6)Force sync in opposite direction at end of sync and Disable compression of Change Notification messages' } + '7' { '(7)Change Notification is Enabled without Compression and Force sync in opposite direction at end of sync' } + Default { "Unknown siteLink option: $($Item.Options)" } + } + 'Sites' = $SiteArray -join "; " + 'Protected From Accidental Deletion' = ConvertTo-TextYN $Item.ProtectedFromAccidentalDeletion + 'Description' = ConvertTo-EmptyToFiller $Item.Description + } + $OutObj = [pscustomobject]$inobj + + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' } | Set-Style -Style Warning -Property 'Options' + $OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' } | Set-Style -Style Warning -Property 'Protected From Accidental Deletion' + } + + $TableParams = @{ + Name = "Site Links - $($Item.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Site Link Name' | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) -or (($OutObj | Where-Object { $_.'Description' -eq '--' }) -or ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' })))) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + } + BlankLine + } + if ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "Enabling change notification treats an INTER-site replication connection like an INTRA-site connection. Replication between sites with change notification is almost instant. Microsoft recommends using an Option number value of 5 (Change Notification is Enabled without Compression)." + } + BlankLine + } + if ($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "If the Site Links in your Active Directory are not protected from accidental deletion, your environment can experience disruptions that might be caused by accidental bulk deletion of objects." + } + BlankLine + } + } + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (IP Site Links table)" + } + } + } + } else { + Write-PScriboMessage -IsWarning "No IP Site Links information found in $ForestInfo, disabling the section." + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (IP Site Links Section)" } - $OutObj | Sort-Object -Property 'Site Link Name' | Table @TableParams - if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) -or (($OutObj | Where-Object { $_.'Description' -eq '--' }) -or ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' })))) { - Paragraph "Health Check:" -Bold -Underline - BlankLine - if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { - Paragraph { - Text "Best Practice:" -Bold - Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + try { + $IPLinkBridges = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLinkBridge -Filter * -Properties * | Where-Object { $_.InterSiteTransportProtocol -eq "IP" } } + if ($IPLinkBridges) { + Section -Style Heading5 'Site Link Bridges' { + $OutObj = @() + foreach ($Item in $IPLinkBridges) { + try { + $SiteArray = @() + $Sites = $Item.siteLinkList + foreach ($Object in $Sites) { + $SiteName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLink -Identity $using:Object } + $SiteArray += $SiteName.Name + } + $inObj = [ordered] @{ + 'Site Link Bridges Name' = $Item.Name + 'Transport Protocol' = $Item.InterSiteTransportProtocol + 'Site Links' = $SiteArray -join "; " + 'Protected From Accidental Deletion' = ConvertTo-TextYN $Item.ProtectedFromAccidentalDeletion + 'Description' = ConvertTo-EmptyToFiller $Item.Description + } + $OutObj = [pscustomobject]$inobj + + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' } | Set-Style -Style Warning -Property 'Protected From Accidental Deletion' + } + + $TableParams = @{ + Name = "Site Links Bridges - $($Item.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) -or (($OutObj | Where-Object { $_.'Description' -eq '--' })))) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + } + BlankLine + } + if ($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "If the Site Links Bridges 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." + } + BlankLine + } + } + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (IP Site Links Bridges table)" + } + } } - BlankLine + } else { + Write-PScriboMessage -IsWarning "No IP Site Links Bridges information found in $ForestInfo, disabling the section." } - if ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' }) { - Paragraph { - Text "Best Practice:" -Bold - Text "Enabling change notification treats an INTER-site replication connection like an INTRA-site connection. Replication between sites with change notification is almost instant. Microsoft recommends using an Option number value of 5 (Change Notification is Enabled without Compression)." + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (IP Site Links Section)" + } + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (IP)" + } + try { + $IPLink = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLink -Filter * -Properties * | Where-Object { $_.InterSiteTransportProtocol -eq "SMTP" } } + if ($IPLink) { + Section -Style Heading4 'SMTP' { + Paragraph "SMTP replication is used for sites that cannot use the others, but as a general rule, it should never be used. It is reserved when network connections are not always available, therefore, you can schedule replication." + try { + Section -Style Heading5 'Site Links' { + $OutObj = @() + foreach ($Item in $IPLink) { + try { + $SiteArray = @() + $Sites = $Item.siteList + foreach ($Object in $Sites) { + $SiteName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSite -Identity $using:Object } + $SiteArray += $SiteName.Name + } + $inObj = [ordered] @{ + 'Site Link Name' = $Item.Name + 'Cost' = $Item.Cost + 'Replication Frequency' = "$($Item.ReplicationFrequencyInMinutes) min" + 'Transport Protocol' = $Item.InterSiteTransportProtocol + 'Options' = Switch ($Item.Options) { + $null { 'Change Notification is Disabled' } + '0' { '(0)Change Notification is Disabled' } + '1' { '(1)Change Notification is Enabled with Compression' } + '2' { '(2)Force sync in opposite direction at end of sync' } + '3' { '(3)Change Notification is Enabled with Compression and Force sync in opposite direction at end of sync' } + '4' { '(4)Disable compression of Change Notification messages' } + '5' { '(5)Change Notification is Enabled without Compression' } + '6' { '(6)Force sync in opposite direction at end of sync and Disable compression of Change Notification messages' } + '7' { '(7)Change Notification is Enabled without Compression and Force sync in opposite direction at end of sync' } + Default { "Unknown siteLink option: $($Item.Options)" } + } + 'Sites' = $SiteArray -join "; " + 'Protected From Accidental Deletion' = ConvertTo-TextYN $Item.ProtectedFromAccidentalDeletion + 'Description' = ConvertTo-EmptyToFiller $Item.Description + } + $OutObj = [pscustomobject]$inobj + + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' } | Set-Style -Style Warning -Property 'Options' + $OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' } | Set-Style -Style Warning -Property 'Protected From Accidental Deletion' + } + + $TableParams = @{ + Name = "Site Links - $($Item.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Site Link Name' | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) -or (($OutObj | Where-Object { $_.'Description' -eq '--' }) -or ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' })))) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + } + BlankLine + } + if ($OutObj | Where-Object { $_.'Options' -eq 'Change Notification is Disabled' -or $Null -eq 'Options' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "Enabling change notification treats an INTER-site replication connection like an INTRA-site connection. Replication between sites with change notification is almost instant. Microsoft recommends using an Option number value of 5 (Change Notification is Enabled without Compression)." + } + BlankLine + } + if ($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "If the Site Links in your Active Directory are not protected from accidental deletion, your environment can experience disruptions that might be caused by accidental bulk deletion of objects." + } + BlankLine + } + } + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (SMTP Site Links table)" + } + } } - BlankLine + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (SMTP Site Links Section)" } - if ($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) { - Paragraph { - Text "Best Practice:" -Bold - Text "If the Site Links in your Active Directory are not protected from accidental deletion, your environment can experience disruptions that might be caused by accidental bulk deletion of objects." + try { + $IPLinkBridges = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLinkBridge -Filter * -Properties * | Where-Object { $_.InterSiteTransportProtocol -eq "SMTP" } } + if ($IPLinkBridges) { + Section -Style Heading5 'Site Link Bridges' { + $OutObj = @() + foreach ($Item in $IPLinkBridges) { + try { + $SiteArray = @() + $Sites = $Item.siteLinkList + foreach ($Object in $Sites) { + $SiteName = Invoke-Command -Session $TempPssSession { Get-ADReplicationSiteLink -Identity $using:Object } + $SiteArray += $SiteName.Name + } + $inObj = [ordered] @{ + 'Site Link Bridges Name' = $Item.Name + 'Transport Protocol' = $Item.InterSiteTransportProtocol + 'Site Links' = $SiteArray -join "; " + 'Protected From Accidental Deletion' = ConvertTo-TextYN $Item.ProtectedFromAccidentalDeletion + 'Description' = ConvertTo-EmptyToFiller $Item.Description + } + $OutObj = [pscustomobject]$inobj + + if ($HealthCheck.Site.BestPractice) { + $OutObj | Where-Object { $_.'Description' -eq '--' } | Set-Style -Style Warning -Property 'Description' + $OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' } | Set-Style -Style Warning -Property 'Protected From Accidental Deletion' + } + + $TableParams = @{ + Name = "Site Links Bridges - $($Item.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Table @TableParams + if ($HealthCheck.Site.BestPractice -and (($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) -or (($OutObj | Where-Object { $_.'Description' -eq '--' })))) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + if ($OutObj | Where-Object { $_.'Description' -eq '--' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "It is a general rule of good practice to establish well-defined descriptions. This helps to speed up the fault identification process, as well as enabling better documentation of the environment." + } + BlankLine + } + if ($OutObj | Where-Object { $_.'Protected From Accidental Deletion' -eq 'No' }) { + Paragraph { + Text "Best Practice:" -Bold + Text "If the Site Links Bridges 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." + } + BlankLine + } + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (SMTP Site Links Bridges table)" + } + } + } + } else { + Write-PScriboMessage -IsWarning "No SMTP Site Links Bridges information found in $ForestInfo, disabling the section." } - BlankLine + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (SMTP Site Links Section)" } } - - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Site Links)" + } else { + Write-PScriboMessage -IsWarning "No SMTP Site Links information found in $ForestInfo, disabling the section." } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (SMTP)" } } } else { - Write-PScriboMessage -IsWarning "No Site Links information found in $ForestInfo, disabling the section." + Write-PScriboMessage -IsWarning "No SMTP Site Links information found in $ForestInfo, disabling the section." } } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Site Subnets)" @@ -396,7 +689,6 @@ function Get-AbrADSite { try { $OutObj = @() foreach ($Domain in $ADSystem.Domains | Where-Object { $_ -notin $Options.Exclude.Domains }) { - Write-PScriboMessage "Discovered AD Domain Sysvol Replication information from $Domain." $DomainInfo = Invoke-Command -Session $TempPssSession { Get-ADDomain $using:Domain -ErrorAction Stop } foreach ($DC in ($DomainInfo.ReplicaDirectoryServers | Where-Object { $_ -notin $Options.Exclude.DCs })) { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { @@ -407,7 +699,6 @@ function Get-AbrADSite { } try { - Write-PScriboMessage "Collecting Sysvol Replication information from $($Domain)." $inObj = [ordered] @{ 'DC Name' = $DC.split(".", 2)[0] 'Replication Status' = Switch ($Replication.State) { diff --git a/Src/Private/Get-AbrADSiteReplication.ps1 b/Src/Private/Get-AbrADSiteReplication.ps1 index 5e57c12..05d6ec5 100644 --- a/Src/Private/Get-AbrADSiteReplication.ps1 +++ b/Src/Private/Get-AbrADSiteReplication.ps1 @@ -29,7 +29,7 @@ function Get-AbrADSiteReplication { process { $DCs = Invoke-Command -Session $TempPssSession -ScriptBlock { Get-ADDomain -Identity $using:Domain | Select-Object -ExpandProperty ReplicaDirectoryServers } if ($DCs) { - Write-PScriboMessage "Discovering Active Directory Sites Replication information on $Domain. (Sites Replication)" + Write-PScriboMessage "Collecting Active Directory Sites Replication information on $Domain. (Sites Replication)" try { $ReplInfo = @() foreach ($DC in $DCs) { @@ -37,7 +37,6 @@ function Get-AbrADSiteReplication { $Replication = Invoke-Command -Session $TempPssSession -ScriptBlock { Get-ADReplicationConnection -Server $using:DC -Properties * } if ($Replication) { try { - Write-PScriboMessage "Collecting Active Directory Sites Replication information on $DC. (Sites Replication Connection)" foreach ($Repl in $Replication) { try { $inObj = [ordered] @{ @@ -119,15 +118,12 @@ 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).ReplicaDirectoryServers | Select-Object -First 1 } $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) { Section -Style Heading4 'Replication Status' { $OutObj = @() - Write-PScriboMessage "Collecting Active Directory Replication Status from $($Domain). (Replication Status)" foreach ($Status in $RepStatus) { try { $inObj = [ordered] @{ diff --git a/Src/Private/Get-AbrADTrust.ps1 b/Src/Private/Get-AbrADTrust.ps1 index ba5804b..2bf9d70 100644 --- a/Src/Private/Get-AbrADTrust.ps1 +++ b/Src/Private/Get-AbrADTrust.ps1 @@ -38,7 +38,6 @@ function Get-AbrADTrust { $TrustInfo = @() foreach ($Trust in $Trusts) { try { - Write-PScriboMessage "Collecting Active Directory Domain Trust information from $($Trust.Name)" $inObj = [ordered] @{ 'Name' = $Trust.Name 'Path' = ConvertTo-ADCanonicalName -DN $Trust.DistinguishedName -Domain $Domain diff --git a/Src/Private/Get-AbrDNSSection.ps1 b/Src/Private/Get-AbrDNSSection.ps1 index 99d374c..8a5c32a 100644 --- a/Src/Private/Get-AbrDNSSection.ps1 +++ b/Src/Private/Get-AbrDNSSection.ps1 @@ -19,7 +19,7 @@ function Get-AbrDNSSection { ) begin { - Write-PScriboMessage "Discovering DNS server information from $ForestInfo." + Write-PScriboMessage "Collecting DNS server information from $ForestInfo." } process { @@ -64,7 +64,6 @@ function Get-AbrDNSSection { } } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Name System Information)" - continue } } } diff --git a/Src/Private/Get-AbrDomainSection.ps1 b/Src/Private/Get-AbrDomainSection.ps1 index cc1c5e9..e74c659 100644 --- a/Src/Private/Get-AbrDomainSection.ps1 +++ b/Src/Private/Get-AbrDomainSection.ps1 @@ -19,7 +19,7 @@ function Get-AbrDomainSection { ) begin { - Write-PScriboMessage "Discovering Domain information from $ForestInfo." + Write-PScriboMessage "Collecting Domain information from $ForestInfo." } process { @@ -70,8 +70,13 @@ function Get-AbrDomainSection { BlankLine } if (!$Options.ShowDefinitionInfo) { - Paragraph "The following section provides a summary of the Active Directory Domain Controllers." - BlankLine + if ($InfoLevel.Domain -ge 2) { + Paragraph "The following section provides detailed information about Active Directory domain controllers." + BlankLine + } else { + Paragraph "The following section provides an overview of 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 } } | Sort-Object @@ -107,7 +112,6 @@ function Get-AbrDomainSection { } catch { Write-PScriboMessage -IsWarning "Error: Connecting to remote server $DC failed: WinRM cannot complete the operation. ('DCDiag Information)" Write-PScriboMessage -IsWarning $_.Exception.Message - continue } } try { @@ -122,7 +126,6 @@ function Get-AbrDomainSection { } catch { Write-PScriboMessage -IsWarning "Error: Connecting to remote server $DC failed: WinRM cannot complete the operation. (ADInfrastructureService)" Write-PScriboMessage -IsWarning $_.Exception.Message - continue } } } @@ -135,7 +138,6 @@ function Get-AbrDomainSection { } } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Active Directory Domain)" - continue } } } diff --git a/Src/Private/Get-AbrForestSection.ps1 b/Src/Private/Get-AbrForestSection.ps1 index c19fa8c..f655af1 100644 --- a/Src/Private/Get-AbrForestSection.ps1 +++ b/Src/Private/Get-AbrForestSection.ps1 @@ -19,7 +19,7 @@ function Get-AbrForestSection { ) begin { - Write-PScriboMessage "Discovering Forest information from $ForestInfo." + Write-PScriboMessage "Collecting Forest information from $ForestInfo." } process { diff --git a/Src/Private/Get-AbrPKISection.ps1 b/Src/Private/Get-AbrPKISection.ps1 index a900a19..5556304 100644 --- a/Src/Private/Get-AbrPKISection.ps1 +++ b/Src/Private/Get-AbrPKISection.ps1 @@ -19,7 +19,7 @@ function Get-AbrPKISection { ) begin { - Write-PScriboMessage "Discovering PKI infrastructure information from $ForestInfo." + Write-PScriboMessage "Collecting PKI infrastructure information from $ForestInfo." } process { @@ -33,7 +33,6 @@ function Get-AbrPKISection { if ($CurrentMachineADDomain.Name -in $ADSystem.Domains) { Write-PScriboMessage "Current PC Domain $($CurrentMachineADDomain.Name) is in the Forrest Domain list of $($ADSystem.Name). Enabling Certificate Authority section" try { - Write-PScriboMessage "Collecting Certification Authority information from $($System.split(".")[0])" $script:CAs = Get-CertificationAuthority -Enterprise } catch { Write-PScriboMessage -IsWarning $_.Exception.Message From ce2a5ba29e69a8fedd637eb8f5a1fb769c289a74 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 17 Feb 2024 00:50:13 -0400 Subject: [PATCH 04/17] Updated Changelog --- CHANGELOG.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c43b68e..335f7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.8.1] - 2024-02-11 +## [0.8.1] - 2024-02-18 ### Added @@ -14,13 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Remove Graphviz install check code. +- Remove Graphviz install check code - Code cleanup +- Improved script logging +- Increased Diagrammer.Microsoft.AD module version requirements ### Fixed -- Improve error handling on Forest Diagram section. -- Fix issues with Diagrammer.Microsoft.AD module. +- Improve error handling on Forest Diagram section +- Fix issues with Diagrammer.Microsoft.AD module +- Fix DNS section not getting data when there are unavailable DC servers - Fix [#154](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/154) - Fix [#153](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/153) - Fix [#152](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/152) From 5b2e8e78e63914fbeaaa9066d103806124ffc1cd Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 17 Feb 2024 00:51:52 -0400 Subject: [PATCH 05/17] Fix Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 335f7ad..203981f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove Graphviz install check code - Code cleanup -- Improved script logging -- Increased Diagrammer.Microsoft.AD module version requirements +- Improve script logging +- Increase Diagrammer.Microsoft.AD module version requirements ### Fixed From 54168dfb728071f83624876a8e0ce52fd0ab2f7e Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 20 Feb 2024 15:51:39 -0400 Subject: [PATCH 06/17] Fix DCDIAG and added Diagrammer.Core Module dependency --- .github/workflows/Release.yml | 4 ++++ Src/Private/Get-AbrADDCDiag.ps1 | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index ab0c84a..206ce9b 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -25,6 +25,10 @@ jobs: shell: pwsh run: | Install-Module -Name PScriboCharts -Repository PSGallery -Force + - name: Install Diagrammer.Core module + shell: pwsh + run: | + Install-Module -Name Diagrammer.Core -Repository PSGallery -Force - name: Install Diagrammer.Microsoft.AD module shell: pwsh run: | diff --git a/Src/Private/Get-AbrADDCDiag.ps1 b/Src/Private/Get-AbrADDCDiag.ps1 index 7f25f4e..22658ff 100644 --- a/Src/Private/Get-AbrADDCDiag.ps1 +++ b/Src/Private/Get-AbrADDCDiag.ps1 @@ -30,7 +30,7 @@ function Get-AbrADDCDiag { } process { - if ($DC) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { try { $DCDIAG = Invoke-DcDiag -DomainController $DC if ($DCDIAG) { @@ -92,9 +92,10 @@ function Get-AbrADDCDiag { } catch { Write-PScriboMessage -IsWarning "Active Directory DCDiag Section: $($_.Exception.Message)" } + } else { + Write-PScriboMessage -IsWarning "Active Directory DCDiag Section: Unable to connect to DC server $DC." } } end {} - } \ No newline at end of file From 68ee3fd00e2c7331c7a70d335b6caeb3ade6e68c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 20 Feb 2024 22:17:20 -0400 Subject: [PATCH 07/17] Increased Diagrammer.Microsoft.AD minimum version required --- .github/workflows/Release.yml | 4 ---- AsBuiltReport.Microsoft.AD.psd1 | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 206ce9b..ab0c84a 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -25,10 +25,6 @@ jobs: shell: pwsh run: | Install-Module -Name PScriboCharts -Repository PSGallery -Force - - name: Install Diagrammer.Core module - shell: pwsh - run: | - Install-Module -Name Diagrammer.Core -Repository PSGallery -Force - name: Install Diagrammer.Microsoft.AD module shell: pwsh run: | diff --git a/AsBuiltReport.Microsoft.AD.psd1 b/AsBuiltReport.Microsoft.AD.psd1 index 386eccf..cef0d82 100644 --- a/AsBuiltReport.Microsoft.AD.psd1 +++ b/AsBuiltReport.Microsoft.AD.psd1 @@ -66,7 +66,7 @@ }, @{ ModuleName = 'Diagrammer.Microsoft.AD'; - ModuleVersion = '0.1.7' + ModuleVersion = '0.1.9' } ) From 18e7a561c66208a024a337f42d7538a8a01d9d22 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 24 Feb 2024 00:06:14 -0400 Subject: [PATCH 08/17] Improved DC exclude logic --- Src/Private/Get-AbrDomainSection.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Src/Private/Get-AbrDomainSection.ps1 b/Src/Private/Get-AbrDomainSection.ps1 index e74c659..7381f7a 100644 --- a/Src/Private/Get-AbrDomainSection.ps1 +++ b/Src/Private/Get-AbrDomainSection.ps1 @@ -92,7 +92,7 @@ function Get-AbrDomainSection { if ($DCStatus -eq $false) { Write-PScriboMessage -IsWarning "Unable to connect to $DC. Removing it from the $Domain report" } - if (($DC -notin $Options.Exclude.DCs) -and $DCStatus) { + if ($DCStatus) { Get-AbrADDCRoleFeature -DC $DC } } @@ -104,7 +104,7 @@ function Get-AbrDomainSection { 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 2)) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Get-AbrADDCDiag -Domain $Domain -DC $DC } } @@ -118,7 +118,7 @@ function Get-AbrDomainSection { Section -Style Heading4 "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 2)) { + if (Test-Connection -ComputerName $DC -Quiet -Count 2) { Get-AbrADInfrastructureService -DC $DC } } From ce73d461184037073a056bfc730187c0cc1783ac Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 24 Feb 2024 00:07:34 -0400 Subject: [PATCH 09/17] Improved Diagram percent logic --- AsBuiltReport.Microsoft.AD.psd1 | 4 ++-- README.md | 1 - Src/Private/Get-AbrADForest.ps1 | 3 ++- Src/Private/Get-AbrADSite.ps1 | 11 +++++----- Src/Private/SharedUtilsFunctions.ps1 | 31 ---------------------------- 5 files changed, 10 insertions(+), 40 deletions(-) diff --git a/AsBuiltReport.Microsoft.AD.psd1 b/AsBuiltReport.Microsoft.AD.psd1 index cef0d82..3224f67 100644 --- a/AsBuiltReport.Microsoft.AD.psd1 +++ b/AsBuiltReport.Microsoft.AD.psd1 @@ -66,7 +66,7 @@ }, @{ ModuleName = 'Diagrammer.Microsoft.AD'; - ModuleVersion = '0.1.9' + ModuleVersion = '0.2.0' } ) @@ -131,7 +131,7 @@ } # End of PrivateData hashtable # HelpInfo URI of this module - # HelpInfoURI = '' + HelpInfoURI = 'https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD#readme' # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' diff --git a/README.md b/README.md index 3a4201d..fb766ee 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,6 @@ The **Options** schema allows certain options within the report to be toggled on | Include.DCs | Array List | Empty | Allow only a list of Active Directory Domain FQDN to document. | | EnableDiagrams | true / false | true | Toggle to enable/disable of Infrastructure Diagrams | - ### InfoLevel The **InfoLevel** schema allows configuration of each section of the report at a granular level. The following sections can be set. diff --git a/Src/Private/Get-AbrADForest.ps1 b/Src/Private/Get-AbrADForest.ps1 index 91eda9b..4a1ed50 100644 --- a/Src/Private/Get-AbrADForest.ps1 +++ b/Src/Private/Get-AbrADForest.ps1 @@ -114,8 +114,9 @@ function Get-AbrADForest { } if ($Graph) { + If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } Section -Style Heading3 "Forest Diagram." { - Image -Base64 $Graph -Text "Forest Diagram" -Percent (Get-ImagePercent -Graph $Graph) -Align Center + Image -Base64 $Graph -Text "Forest Diagram" -Percent $ImagePrty -Align Center Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 } BlankLine -Count 2 diff --git a/Src/Private/Get-AbrADSite.ps1 b/Src/Private/Get-AbrADSite.ps1 index 752ee59..de9ed8d 100644 --- a/Src/Private/Get-AbrADSite.ps1 +++ b/Src/Private/Get-AbrADSite.ps1 @@ -162,10 +162,10 @@ function Get-AbrADSite { $inObj = [ordered] @{ 'Subnet' = $Item.Name 'Description' = ConvertTo-EmptyToFiller $Item.Description - 'Sites' = & { - try { - $Item.Site.Split(",")[0].SubString($Item.Site.Split(",")[0].IndexOf("=") + 1) - } catch { "No site assigned" } + 'Sites' = Switch ([string]::IsNullOrEmpty($Item.Site)) { + $true { "No site assigned" } + $false { $Item.Site.Split(",")[0].SubString($Item.Site.Split(",")[0].IndexOf("=") + 1) } + default { 'Unknown' } } } $OutObj += [pscustomobject]$inObj @@ -291,8 +291,9 @@ function Get-AbrADSite { } if ($Graph) { + If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } Section -Style Heading4 "Site Inventory Diagram." { - Image -Base64 $Graph -Text "Site Inventory Diagram" -Percent (Get-ImagePercent -Graph $Graph) -Align Center + Image -Base64 $Graph -Text "Site Inventory Diagram" -Percent $ImagePrty -Align Center Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 } BlankLine -Count 2 diff --git a/Src/Private/SharedUtilsFunctions.ps1 b/Src/Private/SharedUtilsFunctions.ps1 index 67d11f6..33be41c 100644 --- a/Src/Private/SharedUtilsFunctions.ps1 +++ b/Src/Private/SharedUtilsFunctions.ps1 @@ -1866,37 +1866,6 @@ function get-Severity { } } } - -function Get-ImagePercent { - <# - .SYNOPSIS - Used by As Built Report to get base64 image percentage calculated from image width. - This low the diagram image to fit the report page margins - .DESCRIPTION - .NOTES - Version: 0.1.0 - Author: Jonathan Colon - .EXAMPLE - .LINK - #> - [CmdletBinding()] - [OutputType([System.Int32])] - Param - ( - [Parameter ( - Position = 0, - Mandatory)] - [string] - $Graph - ) - $Image_FromStream = [System.Drawing.Image]::FromStream((New-Object System.IO.MemoryStream(, [convert]::FromBase64String($Graph)))) - If ($Image_FromStream.Width -gt 1500) { - return 10 - } else { - return 50 - } -} # end - Function Get-ADExchangeServer { <# .SYNOPSIS From 9ead4f00035206b42252bba0e36086b4e9660aae Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 28 Feb 2024 12:45:09 -0400 Subject: [PATCH 10/17] Fix chart temporary save location --- Src/Private/SharedUtilsFunctions.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Src/Private/SharedUtilsFunctions.ps1 b/Src/Private/SharedUtilsFunctions.ps1 index 33be41c..60fac85 100644 --- a/Src/Private/SharedUtilsFunctions.ps1 +++ b/Src/Private/SharedUtilsFunctions.ps1 @@ -2006,7 +2006,9 @@ function Get-PieChart { } Add-ChartTitle @addChartTitleParams - $ChartImage = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru + $TempPath = Resolve-Path ([System.IO.Path]::GetTempPath()) + + $ChartImage = Export-Chart -Chart $exampleChart -Path $TempPath.Path -Format "PNG" -PassThru $Base64Image = [convert]::ToBase64String((Get-Content $ChartImage -Encoding byte)) @@ -2090,7 +2092,9 @@ function Get-ColumnChart { } Add-ChartTitle @addChartTitleParams - $ChartImage = Export-Chart -Chart $exampleChart -Path (Get-Location).Path -Format "PNG" -PassThru + $TempPath = Resolve-Path ([System.IO.Path]::GetTempPath()) + + $ChartImage = Export-Chart -Chart $exampleChart -Path $TempPath.Path -Format "PNG" -PassThru if ($PassThru) { Write-Output -InputObject $chartFileItem From 2adf88f08a90f9bc1a29c4efdaae564b2490cd2e Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 14 May 2024 23:40:18 -0400 Subject: [PATCH 11/17] Add Object Section #159 --- Src/Private/Get-AbrADDomainObject.ps1 | 1626 ++++++++++++++----------- 1 file changed, 898 insertions(+), 728 deletions(-) diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index e91797c..85f65cf 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -28,907 +28,1077 @@ function Get-AbrADDomainObject { } process { - try { - Section -Style Heading3 'Domain Object Stats' { - if ($Domain) { + Section -Style Heading3 'Domain Objects' { + Paragraph "The following section details information about computers, groups and users objects found in $($Domain) " + try { + 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", "AccountNotDelegated", "EmailAddress") + $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 } + $script:GroupOBj = Invoke-Command -Session $TempPssSession { (Get-ADGroup -Server $using:DC -Filter * -SearchBase (Get-ADDomain -Identity $using:Domain).distinguishedName) } + $script:DomainController = Invoke-Command -Session $TempPssSession { (Get-ADDomainController -Server $using:DC -Filter *) | Select-Object name | Measure-Object } + $script:GC = Invoke-Command -Session $TempPssSession { (Get-ADDomainController -Server $using:DC -Filter { IsGlobalCatalog -eq "True" }) | Select-Object name | Measure-Object } + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" + } + try { + Section -Style Heading4 'User Objects' { 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", "AccountNotDelegated", "EmailAddress") - $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 } - $script:GroupOBj = Invoke-Command -Session $TempPssSession { (Get-ADGroup -Server $using:DC -Filter * -SearchBase (Get-ADDomain -Identity $using:Domain).distinguishedName) } - $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 } + $OutObj = @() + $inObj = [ordered] @{ + 'Users' = ($Users | Measure-Object).Count + 'Privileged Users' = ($PrivilegedUsers | Measure-Object).Count + } + $OutObj += [pscustomobject]$inobj - try { - $OutObj = @() - $inObj = [ordered] @{ - 'Computers' = $Computers.Count - 'Servers' = $Servers.Count - } - $OutObj += [pscustomobject]$inobj + $TableParams = @{ + Name = "User - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { - $TableParams = @{ - Name = "Computers - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - if ($Options.EnableCharts) { - try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'ComputersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'ComputersObject' -ChartTitleText 'Computers Count' + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'UsersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'UsersObject' -ChartTitleText 'User Objects' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Computers Object Count Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (User Object Count Chart)" } - if ($OutObj) { - Section -ExcludeFromTOC -Style NOTOCHeading3 'Computers' { - if ($chartFileItem) { - Image -Text 'Computers Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem - } - $OutObj | Table @TableParams + } + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading4 'Users' { + if ($chartFileItem) { + Image -Text 'Users Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem } + $OutObj | Table @TableParams } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) } - try { - $OutObj = @() - $inObj = [ordered] @{ - 'Domain Controller' = $DomainController.Count - 'Global Catalog' = $GC.Count - } - $OutObj += [pscustomobject]$inobj - - $TableParams = @{ - Name = "Domain Controller - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - if ($Options.EnableCharts) { - try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' - - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'DomainControllerObject' -XField 'Name' -YField 'value' -ChartLegendName 'Category' -ChartTitleName 'DomainControllerObject' -ChartTitleText 'Domain Controller Object Count' + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) + } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Count Chart)" + $OutObj = @() + $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 } + $passwordNeverExpires = $Users | Where-Object { $_.passwordNeverExpires -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') + if ($Categories) { + foreach ($Category in $Categories) { + try { + if ($Category -eq 'Total Users') { + $Values = $Users + } elseif ($Category -eq 'Cannot Change Password') { + $Values = $CannotChangePassword + } elseif ($Category -eq 'Must Change Password at Logon') { + $Values = $PasswordNextLogon + } 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') { + $Values = $SmartcardLogonRequired + } elseif ($Category -eq 'Never Logged in') { + $Values = $NeverloggedIn + } elseif ($Category -eq 'Dormant (> 90 days)') { + $Values = $Dormant + } elseif ($Category -eq 'Password Not Required') { + $Values = $PasswordNotRequired + } elseif ($Category -eq 'Account Expired') { + $Values = $AccountExpired + } elseif ($Category -eq 'Account Lockout') { + $Values = $AccountLockout + } elseif ($Category -eq 'SidHistory') { + $Values = $SidHistory } - } - if ($OutObj) { - Section -ExcludeFromTOC -Style NOTOCHeading3 'Domain Controller' { - if ($chartFileItem) { - Image -Text 'Domain Controller Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + $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) } + } + 'Disabled' = ($Values.Enabled -eq $False | Measure-Object).Count + 'Disabled %' = Switch ($Users.Count) { + 0 { '0' } + $Null { '0' } + default { [math]::Round((($Values.Enabled -eq $False | Measure-Object).Count / $Users.Count * 100), 2) } + } + 'Total' = ($Values | Measure-Object).Count + 'Total %' = Switch ($Users.Count) { + 0 { '0' } + $Null { '0' } + default { [math]::Round((($Values | Measure-Object).Count / $Users.Count * 100), 2) } } - $OutObj | Table @TableParams + } + $OutObj += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of User Accounts)" } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) } - try { - $OutObj = @() - $inObj = [ordered] @{ - 'Users' = ($Users | Measure-Object).Count - 'Privileged Users' = ($PrivilegedUsers | Measure-Object).Count - 'Groups' = ($GroupOBj | Measure-Object).Count - } - $OutObj += [pscustomobject]$inobj - $TableParams = @{ - Name = "User - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - if ($Options.EnableCharts) { - try { + $TableParams = @{ + Name = "Status of User Accounts - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 28, 12, 12, 12, 12, 12, 12 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' + $sampleData = $OutObj - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'UsersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'UsersObject' -ChartTitleText 'Users Object Count' + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofUsersAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofUsersAccounts' -ChartTitleText 'Status of Users Accounts' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (User Object Count Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Users Accounts Chart)" + } + } + } + if ($OutObj) { + Section -Style Heading5 'Status of Users Accounts' { + if ($chartFileItem) { + Image -Text 'Status of Users Accounts - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem } - if ($OutObj) { - Section -ExcludeFromTOC -Style NOTOCHeading3 'Users' { - if ($chartFileItem) { - Image -Text 'Users Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + $OutObj | Table @TableParams + } + } + + if ($InfoLevel.Domain -ge 4) { + try { + Section -Style Heading4 'Users Inventory' { + $OutObj = @() + foreach ($User in $Users) { + try { + $Groups = Invoke-Command -Session $TempPssSession -ScriptBlock { (Get-ADPrincipalGroupMembership ($using:User).SamAccountName | Sort-Object | Select-Object -ExpandProperty Name) -join ', ' } + $inObj = [ordered] @{ + 'Name' = ConvertTo-EmptyToFiller $User.DisplayName + 'Logon Name' = $User.SamAccountName + 'Member Of Groups' = Switch ([string]::IsNullOrEmpty($Groups)) { + $true { '--' } + $false { $Groups } + default { 'Unknown' } + } + } + $OutObj += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Users Objects Table)" } - $OutObj | Table @TableParams } + + $TableParams = @{ + Name = "Users - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 33, 33, 34 + } + + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $OutObj | Sort-Object -Property 'Name' | Table @TableParams } + } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Users Objects Section)" } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" } } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Object Stats)" - } - try { - $OutObj = @() - $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 } - $passwordNeverExpires = $Users | Where-Object { $_.passwordNeverExpires -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') - if ($Categories) { - foreach ($Category in $Categories) { + try { + Section -Style Heading4 'Group Objects' { try { - if ($Category -eq 'Total Users') { - $Values = $Users - } elseif ($Category -eq 'Cannot Change Password') { - $Values = $CannotChangePassword - } elseif ($Category -eq 'Must Change Password at Logon') { - $Values = $PasswordNextLogon - } 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') { - $Values = $SmartcardLogonRequired - } elseif ($Category -eq 'Never Logged in') { - $Values = $NeverloggedIn - } elseif ($Category -eq 'Dormant (> 90 days)') { - $Values = $Dormant - } elseif ($Category -eq 'Password Not Required') { - $Values = $PasswordNotRequired - } elseif ($Category -eq 'Account Expired') { - $Values = $AccountExpired - } elseif ($Category -eq 'Account Lockout') { - $Values = $AccountLockout - } elseif ($Category -eq 'SidHistory') { - $Values = $SidHistory - } + $OutObj = @() $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) } - } - '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) } + 'Security Groups' = ($GroupOBj | Where-Object { $_.GroupCategory -eq "Security" } | Measure-Object).Count + 'Distribution Groups' = ($GroupOBj | Where-Object { $_.GroupCategory -eq "Distribution" } | Measure-Object).Count + } + $OutObj += [pscustomobject]$inobj + + $TableParams = @{ + Name = "Group Categories - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { + + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Name' + + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'GroupCategoryObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'GroupCategoryObject' -ChartTitleText 'Group Categories' -ReversePalette $True + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Category Object Chart)" } - 'Total' = ($Values | Measure-Object).Count - 'Total %' = Switch ($Users.Count) { - 0 { '0' } - $Null { '0' } - default { [math]::Round((($Values | Measure-Object).Count / $Users.Count * 100), 2) } + } + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading4 'Groups Categories' { + if ($chartFileItem) { + Image -Text 'Groups Categories Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + } + $OutObj | Table @TableParams } - } - $OutObj += [pscustomobject]$inobj } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of User Accounts)" + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - } - - $TableParams = @{ - Name = "Status of User Accounts - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 28, 12, 12, 12, 12, 12, 12 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - if ($Options.EnableCharts) { try { + $OutObj = @() + $inObj = [ordered] @{ + 'Domain Locals' = ($GroupOBj | Where-Object { $_.GroupScope -eq "DomainLocal" } | Measure-Object).Count + 'Globals' = ($GroupOBj | Where-Object { $_.GroupScope -eq "Global" } | Measure-Object).Count + 'Universal' = ($GroupOBj | Where-Object { $_.GroupScope -eq "Universal" } | Measure-Object).Count + } + $OutObj += [pscustomobject]$inobj - $sampleData = $OutObj + $TableParams = @{ + Name = "Group Scopes - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { + + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Name' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofUsersAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofUsersAccounts' -ChartTitleText 'Status of Users Accounts' + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'GroupCategoryObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'GroupScopesObject' -ChartTitleText 'Group Scopes' -ReversePalette $True + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Scopes Object Chart)" + } + } + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading4 'Groups Scopes' { + if ($chartFileItem) { + Image -Text 'Groups Scopes Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + } + $OutObj | Table @TableParams + } + } } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Users Accounts Chart)" - } - } - } - if ($OutObj) { - Section -Style Heading3 'Status of Users Accounts' { - if ($chartFileItem) { - Image -Text 'Status of Users Accounts - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - $OutObj | Table @TableParams - } - } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) - } - try { - Section -Style Heading3 'Privileged Groups' { - $OutObj = @() - if ($Domain) { - try { - $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' - } - if ($GroupsSID) { - if ($InfoLevel.Domain -eq 1) { - Paragraph "The following session summarizes the counts of users within the privileged groups." - BlankLine - foreach ($GroupSID in $GroupsSID) { + if ($InfoLevel.Domain -ge 4) { + try { + Section -Style Heading4 'Groups Inventory' { + $OutObj = @() + foreach ($Group in $GroupOBj) { try { - $Group = $GroupOBj | Where-Object { $_.SID -like $GroupSID } - if ($Group) { - $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 + $UserCount = Invoke-Command -Session $TempPssSession { (Get-ADGroupMember -Server $using:DC -Identity ($using:Group).Name | Measure-Object).Count } + $inObj = [ordered] @{ + 'Name' = $Group.Name + 'Category' = $Group.GroupCategory + 'Scope' = $Group.GroupScope + 'User Count' = $UserCount } + $OutObj += [pscustomobject]$inobj } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Groups Objects Table)" } } - if ($HealthCheck.Domain.Security) { - 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 = @{ - Name = "Privileged Groups - $($Domain.ToString().ToUpper())" + Name = "Groups - $($Domain.ToString().ToUpper())" List = $false - ColumnWidths = 60, 40 + ColumnWidths = 35, 25, 25, 15 } + 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 }) -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 "Security Best Practice:" -Bold - if ($OutObj | Where-Object { $_.'Group Name' -eq '*Schema Admins' -and $_.Count -gt 1 }) { + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + } + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Groups Objects Section)" + } + } + Section -Style Heading5 'Privileged Groups' { + $OutObj = @() + if ($Domain) { + try { + $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' + } + if ($GroupsSID) { + if ($InfoLevel.Domain -eq 1) { + Paragraph "The following session summarizes the counts of users within the privileged groups." 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." + foreach ($GroupSID in $GroupsSID) { + try { + $Group = $GroupOBj | Where-Object { $_.SID -like $GroupSID } + if ($Group) { + $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 + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" + } } - } - 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 ($HealthCheck.Domain.Security) { + 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 } - } - 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." + + $TableParams = @{ + Name = "Privileged Groups - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 60, 40 } - } - } - } else { - Paragraph "The following session details the members users within the privilege groups." - BlankLine - foreach ($GroupSID in $GroupsSID) { - try { - $Group = $GroupOBj | Where-Object { $_.SID -like $GroupSID } - if ($Group) { - $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 NOTOCHeading4 "$($Group.Name) ($(($GroupObjects | Measure-Object).count) Members)" { - $OutObj = @() - foreach ($GroupObject in $GroupObjects) { - try { - $inObj = [ordered] @{ - 'Name' = $GroupObject.SamAccountName - 'Last Logon Date' = Switch ([string]::IsNullOrEmpty($GroupObject.LastLogonDate)) { - $true { "--" } - $false { $GroupObject.LastLogonDate.ToShortDateString() } - default { "Unknown" } + 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 }) -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 "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." + BlankLine + foreach ($GroupSID in $GroupsSID) { + try { + $Group = $GroupOBj | Where-Object { $_.SID -like $GroupSID } + if ($Group) { + $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 NOTOCHeading4 "$($Group.Name) ($(($GroupObjects | Measure-Object).count) Members)" { + $OutObj = @() + foreach ($GroupObject in $GroupObjects) { + try { + $inObj = [ordered] @{ + 'Name' = $GroupObject.SamAccountName + 'Last Logon Date' = Switch ([string]::IsNullOrEmpty($GroupObject.LastLogonDate)) { + $true { "--" } + $false { $GroupObject.LastLogonDate.ToShortDateString() } + default { "Unknown" } + } + 'Password Never Expires' = ConvertTo-TextYN $GroupObject.passwordNeverExpires + 'Account Enabled' = ConvertTo-TextYN $GroupObject.Enabled + } + $OutObj += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" + } - 'Password Never Expires' = ConvertTo-TextYN $GroupObject.passwordNeverExpires - 'Account Enabled' = ConvertTo-TextYN $GroupObject.Enabled } - $OutObj += [pscustomobject]$inobj - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" - } - } - - 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 { $_.'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 = @{ - 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) -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 "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 ($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 { $_.'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' + } } - } - 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." + + $TableParams = @{ + Name = "$($Group.Name) - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 50, 20, 15, 15 } - } - if ($OutObj | Where-Object { $_.'Password Never Expires' -eq '**Yes' }) { - BlankLine - Paragraph { - Text "**Ensure there aren't any account with weak security posture." + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" } - } - if ($OutObj | Where-Object { $_.'Last Logon Date' -match "\*" }) { - BlankLine - Paragraph { - Text "*Regularly check for and remove inactive privileged user accounts in Active Directory." + $OutObj | Sort-Object -Property 'Name' | Table @TableParams + 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 "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." + } + } } } } } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Privileged Group in Active Directory item)" } } - } 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 = @() - $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 } - $SidHistory = $Computers.SIDHistory - $Categories = @('Total Computers', 'Dormant (> 90 days)', 'Password Age (> 30 days)', 'SidHistory') - if ($Categories) { - foreach ($Category in $Categories) { - try { - if ($Category -eq 'Total Computers') { - $Values = $Computers - } elseif ($Category -eq 'Dormant (> 90 days)') { - $Values = $Dormant - } elseif ($Category -eq 'Password Age (> 30 days)') { - $Values = $PasswordAge - } 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) } - } - '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) } - } - 'Total' = ($Values | Measure-Object).Count - 'Total %' = Switch ($Computers.Count) { - 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)" + Section -Style Heading4 'Computer Objects' { + try { + $OutObj = @() + $inObj = [ordered] @{ + 'Computers' = $Computers.Count + 'Servers' = $Servers.Count } - } + $OutObj += [pscustomobject]$inobj - $TableParams = @{ - Name = "Status of Computer Accounts - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 28, 12, 12, 12, 12, 12, 12 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - if ($Options.EnableCharts) { - try { - - $sampleData = $OutObj + $TableParams = @{ + Name = "Computers - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofComputerAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofComputerAccounts' -ChartTitleText 'Status of Computers Accounts' + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'ComputersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'ComputersObject' -ChartTitleText 'Computers Count' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Computers Accounts Chart)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Computers Object Count Chart)" + } } - } - if ($OutObj) { - Section -Style Heading3 'Status of Computer Accounts' { - if ($chartFileItem -and ($OutObj.'Total' | Measure-Object -Sum).Sum -ne 0) { - Image -Text 'Status of Computer Accounts - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + if ($OutObj) { + Section -ExcludeFromTOC -Style NOTOCHeading4 'Computers' { + if ($chartFileItem) { + Image -Text 'Computers Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + } + $OutObj | Table @TableParams } - $OutObj | Table @TableParams } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) - } - try { - Section -Style Heading3 'Operating Systems Count' { - $OutObj = @() - if ($Domain) { - try { - $OSObjects = $Computers | Where-Object { $_.name -like '*' } | Group-Object -Property operatingSystem | Select-Object Name, Count - if ($OSObjects) { - foreach ($OSObject in $OSObjects) { + try { + $OutObj = @() + $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 } + $SidHistory = $Computers.SIDHistory + $Categories = @('Total Computers', 'Dormant (> 90 days)', 'Password Age (> 30 days)', 'SidHistory') + if ($Categories) { + foreach ($Category in $Categories) { + try { + if ($Category -eq 'Total Computers') { + $Values = $Computers + } elseif ($Category -eq 'Dormant (> 90 days)') { + $Values = $Dormant + } elseif ($Category -eq 'Password Age (> 30 days)') { + $Values = $PasswordAge + } elseif ($Category -eq 'SidHistory') { + $Values = $SidHistory + } $inObj = [ordered] @{ - 'Operating System' = Switch ([string]::IsNullOrEmpty($OSObject.Name)) { - $True { 'No OS Specified' } - default { $OSObject.Name } + '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) } } - 'Count' = $OSObject.Count + '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) } + } + 'Total' = ($Values | Measure-Object).Count + 'Total %' = Switch ($Computers.Count) { + 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)" } - 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' - } + } - $TableParams = @{ - Name = "Operating System Count - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 60, 40 + $TableParams = @{ + Name = "Status of Computer Accounts - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 28, 12, 12, 12, 12, 12, 12 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { + + $sampleData = $OutObj + + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofComputerAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofComputerAccounts' -ChartTitleText 'Status of Computers Accounts' -ReversePalette $True + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Computers Accounts Chart)" } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($OutObj) { + Section -Style Heading5 'Status of Computer Accounts' { + if ($chartFileItem -and ($OutObj.'Total' | Measure-Object -Sum).Sum -ne 0) { + Image -Text 'Status of Computer Accounts - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + } + $OutObj | Table @TableParams } - $OutObj | Sort-Object -Property 'Operating System' | Table @TableParams - if ($HealthCheck.Domain.Security -and ($OutObj | Where-Object { $_.'Operating System' -like '* NT*' -or $_.'Operating System' -like '*2000*' -or $_.'Operating System' -like '*2003*' -or $_.'Operating System' -like '*2008*' -or $_.'Operating System' -like '* NT*' -or $_.'Operating System' -like '*2000*' -or $_.'Operating System' -like '* 95*' -or $_.'Operating System' -like '* 7*' -or $_.'Operating System' -like '* 8 *' -or $_.'Operating System' -like '* 98*' -or $_.'Operating System' -like '*XP*' -or $_.'Operating System' -like '* Vista*' })) { - Paragraph "Health Check:" -Bold -Underline - BlankLine - Paragraph { - Text "Security Best Practice:" -Bold - Text "Operating systems that are no longer supported for security updates are not maintained or updated for vulnerabilities leaving them open to potential attack. Organizations must transition to a supported operating system to ensure continued support and to increase the organization security posture" + } + } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) + } + try { + Section -Style Heading5 'Operating Systems Count' { + $OutObj = @() + if ($Domain) { + try { + $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 } + } + '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' + } + + $TableParams = @{ + Name = "Operating System Count - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 60, 40 + } + if ($Report.ShowTableCaptions) { + $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*' })) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Security Best Practice:" -Bold + Text "Operating systems that are no longer supported for security updates are not maintained or updated for vulnerabilities leaving them open to potential attack. Organizations must transition to a supported operating system to ensure continued support and to increase the organization security posture." + } + } } + } 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 Heading3 'Default Domain Password Policy' { - $OutObj = @() - if ($Domain) { + if ($InfoLevel.Domain -ge 4) { try { - $PasswordPolicy = Invoke-Command -Session $TempPssSession { Get-ADDefaultDomainPasswordPolicy -Identity $using:Domain } - if ($PasswordPolicy) { - $inObj = [ordered] @{ - 'Password Must Meet Complexity Requirements' = ConvertTo-TextYN $PasswordPolicy.ComplexityEnabled - 'Path' = ConvertTo-ADCanonicalName -DN $PasswordPolicy.DistinguishedName -Domain $Domain - 'Lockout Duration' = $PasswordPolicy.LockoutDuration.toString("mm' minutes'") - 'Lockout Threshold' = $PasswordPolicy.LockoutThreshold - 'Lockout Observation Window' = $PasswordPolicy.LockoutObservationWindow.toString("mm' minutes'") - 'Max Password Age' = $PasswordPolicy.MaxPasswordAge.toString("dd' days'") - 'Min Password Age' = $PasswordPolicy.MinPasswordAge.toString("dd' days'") - 'Min Password Length' = $PasswordPolicy.MinPasswordLength - 'Enforce Password History' = $PasswordPolicy.PasswordHistoryCount - 'Store Password using Reversible Encryption' = ConvertTo-TextYN $PasswordPolicy.ReversibleEncryptionEnabled + Section -Style Heading4 'Computers Inventory' { + $OutObj = @() + foreach ($Computer in $Computers) { + try { + $inObj = [ordered] @{ + 'Name' = $Computer.Name + 'DNS HostName' = ConvertTo-EmptyToFiller $Computer.DNSHostName + 'Operating System' = ConvertTo-EmptyToFiller $Computer.operatingSystem + 'Status' = Switch ($Computer.Enabled) { + 'True' {'Enabled'} + 'False' {'Disabled'} + default {'Unknown'} + } + } + $OutObj += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Computers Objects Table)" + } } - $OutObj += [pscustomobject]$inobj $TableParams = @{ - Name = "Default Domain Password Policy - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 + Name = "Computers - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 30, 30, 25, 15 } + if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - $OutObj | Table @TableParams + $OutObj | Sort-Object -Property 'Name' | Table @TableParams } + } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Default Domain Password Policy)" + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Computers Objects Section)" } } } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) - } - try { - if ($Domain) { - foreach ($Item in $Domain) { - $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 Heading3 'Fined Grained Password Policies' { - $FGPPInfo = @() - foreach ($FGPP in $PasswordPolicy) { - 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 } - } - $inObj = [ordered] @{ - 'Name' = $FGPP.Name - 'Domain Name' = $Item - 'Complexity Enabled' = ConvertTo-TextYN $FGPP.ComplexityEnabled - 'Path' = ConvertTo-ADCanonicalName -DN $FGPP.DistinguishedName -Domain $Domain - 'Lockout Duration' = $FGPP.LockoutDuration.toString("mm' minutes'") - 'Lockout Threshold' = $FGPP.LockoutThreshold - 'Lockout Observation Window' = $FGPP.LockoutObservationWindow.toString("mm' minutes'") - 'Max Password Age' = $FGPP.MaxPasswordAge.toString("dd' days'") - 'Min Password Age' = $FGPP.MinPasswordAge.toString("dd' days'") - 'Min Password Length' = $FGPP.MinPasswordLength - 'Password History Count' = $FGPP.PasswordHistoryCount - 'Reversible Encryption Enabled' = ConvertTo-TextYN $FGPP.ReversibleEncryptionEnabled - 'Precedence' = $FGPP.Precedence - 'Applies To' = $Accounts -join ", " - } - $FGPPInfo += [pscustomobject]$inobj - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) + try { + Section -Style Heading3 'Default Domain Password Policy' { + $OutObj = @() + if ($Domain) { + try { + $PasswordPolicy = Invoke-Command -Session $TempPssSession { Get-ADDefaultDomainPasswordPolicy -Identity $using:Domain } + if ($PasswordPolicy) { + $inObj = [ordered] @{ + 'Password Must Meet Complexity Requirements' = ConvertTo-TextYN $PasswordPolicy.ComplexityEnabled + 'Path' = ConvertTo-ADCanonicalName -DN $PasswordPolicy.DistinguishedName -Domain $Domain + 'Lockout Duration' = $PasswordPolicy.LockoutDuration.toString("mm' minutes'") + 'Lockout Threshold' = $PasswordPolicy.LockoutThreshold + 'Lockout Observation Window' = $PasswordPolicy.LockoutObservationWindow.toString("mm' minutes'") + 'Maximun Password Age' = $PasswordPolicy.MaxPasswordAge.toString("dd' days'") + 'Minimun Password Age' = $PasswordPolicy.MinPasswordAge.toString("dd' days'") + 'Minimun Password Length' = $PasswordPolicy.MinPasswordLength + 'Enforce Password History' = $PasswordPolicy.PasswordHistoryCount + 'Store Password using Reversible Encryption' = ConvertTo-TextYN $PasswordPolicy.ReversibleEncryptionEnabled } - } + $OutObj += [pscustomobject]$inobj - if ($InfoLevel.Domain -ge 2) { - foreach ($FGPP in $FGPPInfo) { - Section -Style NOTOCHeading4 -ExcludeFromTOC "$($FGPP.Name)" { - $TableParams = @{ - Name = "Fined Grained Password Policies - $($FGPP.Name)" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - $FGPP | Table @TableParams - } + if ($HealthCheck.Domain.Security -and ($PasswordPolicy.MaxPasswordAge -gt 90)) { + $OutObj | Set-Style -Style Warning -Property 'Maximun Password Age' } - } else { + $TableParams = @{ - Name = "Fined Grained Password Policies - $($Domain.ToString().ToUpper())" - List = $false - Columns = 'Name', 'Lockout Duration', 'Max Password Age', 'Min Password Age', 'Min Password Length', 'Password History Count' - ColumnWidths = 20, 20, 15, 15, 15, 15 + Name = "Default Domain Password Policy - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - $FGPPInfo | Table @TableParams + $OutObj | Table @TableParams + + if ($HealthCheck.Domain.Security -and ($PasswordPolicy.MaxPasswordAge -gt 90)) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Security Best Practice:" -Bold + Text "The MS-ISAC recommends organizations establish a standard for the creation, maintenance, and storage of strong passwords. A Password policies should enforce a maximum password age of between 30 and 90 days." + } + } } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Default Domain Password Policy)" } } } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Fined Grained Password Policies)" - } - - try { - if ($Domain -eq $ADSystem.RootDomain) { - foreach ($Item in $Domain) { - $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 Heading3 'Windows LAPS ' { - $LAPSInfo = @() - try { - $inObj = [ordered] @{ - 'Name' = $LAPS.Name - 'Domain Name' = $Item - 'Enabled' = Switch ($LAPS.Count) { - 0 { 'No' } - default { 'Yes' } + try { + if ($Domain) { + foreach ($Item in $Domain) { + $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 Heading3 'Fined Grained Password Policies' { + $FGPPInfo = @() + foreach ($FGPP in $PasswordPolicy) { + 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 } + } + $inObj = [ordered] @{ + 'Name' = $FGPP.Name + 'Domain Name' = $Item + 'Complexity Enabled' = ConvertTo-TextYN $FGPP.ComplexityEnabled + 'Path' = ConvertTo-ADCanonicalName -DN $FGPP.DistinguishedName -Domain $Domain + 'Lockout Duration' = $FGPP.LockoutDuration.toString("mm' minutes'") + 'Lockout Threshold' = $FGPP.LockoutThreshold + 'Lockout Observation Window' = $FGPP.LockoutObservationWindow.toString("mm' minutes'") + 'Max Password Age' = $FGPP.MaxPasswordAge.toString("dd' days'") + 'Min Password Age' = $FGPP.MinPasswordAge.toString("dd' days'") + 'Min Password Length' = $FGPP.MinPasswordLength + 'Password History Count' = $FGPP.PasswordHistoryCount + 'Reversible Encryption Enabled' = ConvertTo-TextYN $FGPP.ReversibleEncryptionEnabled + 'Precedence' = $FGPP.Precedence + 'Applies To' = $Accounts -join ", " + } + $FGPPInfo += [pscustomobject]$inobj + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) + } } - 'Distinguished Name' = $LAPS.DistinguishedName + if ($InfoLevel.Domain -ge 2) { + foreach ($FGPP in $FGPPInfo) { + Section -Style NOTOCHeading4 -ExcludeFromTOC "$($FGPP.Name)" { + $TableParams = @{ + Name = "Fined Grained Password Policies - $($FGPP.Name)" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $FGPP | Table @TableParams + } + } + } else { + $TableParams = @{ + Name = "Fined Grained Password Policies - $($Domain.ToString().ToUpper())" + List = $false + Columns = 'Name', 'Lockout Duration', 'Max Password Age', 'Min Password Age', 'Min Password Length', 'Password History Count' + ColumnWidths = 20, 20, 15, 15, 15, 15 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $FGPPInfo | Table @TableParams + } } - $LAPSInfo += [pscustomobject]$inobj + } + } + } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Fined Grained Password Policies)" + } - if ($HealthCheck.Domain.Security) { - $LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Warning - } + try { + if ($Domain -eq $ADSystem.RootDomain) { + foreach ($Item in $Domain) { + $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 Heading3 'Windows LAPS ' { + $LAPSInfo = @() + try { + $inObj = [ordered] @{ + 'Name' = $LAPS.Name + 'Domain Name' = $Item + 'Enabled' = Switch ($LAPS.Count) { + 0 { 'No' } + default { 'Yes' } + } + 'Distinguished Name' = $LAPS.DistinguishedName - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) - } + } + $LAPSInfo += [pscustomobject]$inobj + + if ($HealthCheck.Domain.Security) { + $LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Warning + } + + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) + } - if ($InfoLevel.Domain -ge 2) { - foreach ($LAP in $LAPSInfo) { + if ($InfoLevel.Domain -ge 2) { + foreach ($LAP in $LAPSInfo) { + $TableParams = @{ + Name = "Windows LAPS - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $LAP | Table @TableParams + } + } else { $TableParams = @{ - Name = "Windows LAPS - $($Domain.ToString().ToUpper())" - List = $true - ColumnWidths = 40, 60 + Name = "Windows LAPS - $($Domain.ToString().ToUpper())" + List = $false + Columns = 'Name', 'Domain Name', 'Enabled' + ColumnWidths = 34, 33, 33 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - $LAP | Table @TableParams + $LAPSInfo | Table @TableParams } - } else { - $TableParams = @{ - Name = "Windows LAPS - $($Domain.ToString().ToUpper())" - List = $false - Columns = 'Name', 'Domain Name', 'Enabled' - ColumnWidths = 34, 33, 33 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" - } - $LAPSInfo | Table @TableParams - } - if ($HealthCheck.Domain.Security -and ($LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' })) { - Paragraph "Health Check:" -Bold -Underline - BlankLine - Paragraph { - Text "Security Best Practice:" -Bold - Text "LAPS simplifies password management while helping customers implement additional recommended defenses against cyberattacks. In particular, the solution mitigates the risk of lateral escalation that results when customers use the same administrative local account and password combination on their computers. Download, install, and configure Microsoft LAPS or a third-party solution." + if ($HealthCheck.Domain.Security -and ($LAPSInfo | Where-Object { $_.'Enabled' -eq 'No' })) { + Paragraph "Health Check:" -Bold -Underline + BlankLine + Paragraph { + Text "Security Best Practice:" -Bold + Text "LAPS simplifies password management while helping customers implement additional recommended defenses against cyberattacks. In particular, the solution mitigates the risk of lateral escalation that results when customers use the same administrative local account and password combination on their computers. Download, install, and configure Microsoft LAPS or a third-party solution." + } } } } } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Windows LAPS)" } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Windows LAPS)" - } - try { - if ($Domain) { - try { - $GMSA = Invoke-Command -Session $TempPssSession { Get-ADServiceAccount -Server $using:DC -Filter * -Properties * } - if ($GMSA) { - Section -Style Heading3 'gMSA Identities' { - $GMSAInfo = @() - foreach ($Account in $GMSA) { - try { - $inObj = [ordered] @{ - 'Name' = $Account.Name - 'SamAccountName' = $Account.SamAccountName - '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' = 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' = Switch ([string]::IsNullOrEmpty($Account.PasswordLastSet)) { - $true { '--' } - $false { $Account.PasswordLastSet.ToShortDateString() } - default { "Unknown" } + try { + if ($Domain) { + try { + $GMSA = Invoke-Command -Session $TempPssSession { Get-ADServiceAccount -Server $using:DC -Filter * -Properties * } + if ($GMSA) { + Section -Style Heading3 'gMSA Identities' { + $GMSAInfo = @() + foreach ($Account in $GMSA) { + try { + $inObj = [ordered] @{ + 'Name' = $Account.Name + 'SamAccountName' = $Account.SamAccountName + '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' = 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' = Switch ([string]::IsNullOrEmpty($Account.PasswordLastSet)) { + $true { '--' } + $false { $Account.PasswordLastSet.ToShortDateString() } + default { "Unknown" } + } } - } - $GMSAInfo += [pscustomobject]$inobj + $GMSAInfo += [pscustomobject]$inobj - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts Item)" + } catch { + 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' + 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' + } } - } - if ($InfoLevel.Domain -ge 2) { - foreach ($Account in $GMSAInfo) { - Section -Style NOTOCHeading4 -ExcludeFromTOC "$($Account.Name)" { - $TableParams = @{ - Name = "gMSA - $($Account.Name)" - List = $true - ColumnWidths = 40, 60 - } - if ($Report.ShowTableCaptions) { - $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 ($InfoLevel.Domain -ge 2) { + foreach ($Account in $GMSAInfo) { + Section -Style NOTOCHeading4 -ExcludeFromTOC "$($Account.Name)" { + $TableParams = @{ + Name = "gMSA - $($Account.Name)" + List = $true + ColumnWidths = 40, 60 } - 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 ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Account | Where-Object { $_.'Retrieve Managed Password' -eq '***--' }) { + $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 { - 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." + 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 { - $TableParams = @{ - Name = "gMSA - $($Domain.ToString().ToUpper())" - List = $false - 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)" - } - $GMSAInfo | Table @TableParams - 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' -eq "*--" }) { - Paragraph { - Text "Security Best Practice:" -Bold - Text "*Regularly check for and remove inactive group managed service accounts from Active Directory." + } else { + $TableParams = @{ + Name = "gMSA - $($Domain.ToString().ToUpper())" + List = $false + 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)" + } + $GMSAInfo | Table @TableParams + 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' -eq "*--" }) { + 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 Section)" } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Managed Service Accounts Section)" } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) - } - try { - if ($Domain) { - try { - $FSP = Invoke-Command -Session $TempPssSession { Get-ADObject -Server $using:DC -Filter { ObjectClass -eq "foreignSecurityPrincipal" } -Properties msds-principalname, memberof } - if ($FSP) { - Section -Style Heading3 'Foreign Security Principals' { - $FSPInfo = @() - foreach ($Account in $FSP) { - try { - $inObj = [ordered] @{ - 'Name' = $Account.'msds-principalname' - 'Principal Name' = $Account.memberof | ForEach-Object { - if ($Null -ne $_) { - ConvertTo-ADObjectName -DN $_ -Session $TempPssSession -DC $DC - } else { - return "--" + try { + if ($Domain) { + try { + $FSP = Invoke-Command -Session $TempPssSession { Get-ADObject -Server $using:DC -Filter { ObjectClass -eq "foreignSecurityPrincipal" } -Properties msds-principalname, memberof } + if ($FSP) { + Section -Style Heading3 'Foreign Security Principals' { + $FSPInfo = @() + foreach ($Account in $FSP) { + try { + $inObj = [ordered] @{ + 'Name' = $Account.'msds-principalname' + 'Principal Name' = $Account.memberof | ForEach-Object { + if ($Null -ne $_) { + ConvertTo-ADObjectName -DN $_ -Session $TempPssSession -DC $DC + } else { + return "--" + } } } - } - $FSPInfo += [pscustomobject]$inobj + $FSPInfo += [pscustomobject]$inobj - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Foreign Security Principals Item)" + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Foreign Security Principals Item)" + } } - } - $TableParams = @{ - Name = "Foreign Security Principals - $($Domain.ToString().ToUpper())" - List = $false - ColumnWidths = 50, 50 - } - if ($Report.ShowTableCaptions) { - $TableParams['Caption'] = "- $($TableParams.Name)" + $TableParams = @{ + Name = "Foreign Security Principals - $($Domain.ToString().ToUpper())" + List = $false + ColumnWidths = 50, 50 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + $FSPInfo | Table @TableParams } - $FSPInfo | Table @TableParams } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Foreign Security Principals Section)" } - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Foreign Security Principals Section)" } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) } - } catch { - Write-PScriboMessage -IsWarning $($_.Exception.Message) } } From 24d87795fc801267cb9fe2f3f3894f971f020e29 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 14 May 2024 23:41:18 -0400 Subject: [PATCH 12/17] Added Domain and Trust diagram --- Src/Private/Get-AbrADTrust.ps1 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Src/Private/Get-AbrADTrust.ps1 b/Src/Private/Get-AbrADTrust.ps1 index 2bf9d70..467809a 100644 --- a/Src/Private/Get-AbrADTrust.ps1 +++ b/Src/Private/Get-AbrADTrust.ps1 @@ -102,6 +102,26 @@ function Get-AbrADTrust { } $TrustInfo | Table @TableParams } + if ($Options.EnableDiagrams -and ($Domain -eq $ADSystem.RootDomain)) { + try { + try { + $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Trusts + } catch { + Write-PScriboMessage -IsWarning "Domain and Trusts Diagram Graph: $($_.Exception.Message)" + } + + if ($Graph) { + If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } + Section -Style Heading3 "Domain and Trusts Diagram." { + Image -Base64 $Graph -Text "Domain and Trusts Diagram" -Percent $ImagePrty -Align Center + Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 + } + BlankLine -Count 2 + } + } catch { + Write-PScriboMessage -IsWarning "Domain and Trusts Diagram Section: $($_.Exception.Message)" + } + } } } else { Write-PScriboMessage -IsWarning "No Domain Trust information found in $Domain, disabling the section." From cc542d8559c3a251f9cec47c6fb52e812499d179 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Tue, 14 May 2024 23:42:05 -0400 Subject: [PATCH 13/17] Misc Fixes --- CHANGELOG.md | 9 ++-- Src/Private/Get-AbrADDNSInfrastructure.ps1 | 2 +- Src/Private/Get-AbrADDomain.ps1 | 2 +- Src/Private/Get-AbrADDomainController.ps1 | 42 ++++++++++++++-- Src/Private/Get-AbrADForest.ps1 | 6 +-- Src/Private/Get-AbrADOU.ps1 | 4 +- Src/Private/Get-AbrADSecurityAssessment.ps1 | 6 +-- Src/Private/Get-AbrADSite.ps1 | 8 ++-- Src/Private/SharedUtilsFunctions.ps1 | 48 +++++++++++++++---- .../Invoke-AsBuiltReport.Microsoft.AD.ps1 | 7 +++ 10 files changed, 106 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 203981f..179677e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.8.1] - 2024-02-18 +## [0.8.1] - Unreleased ### Added -- Site Inventory Diagram +- Site Topology diagram +- Domain and Trust diagram - Foreign Security Principals section ### Changed @@ -18,10 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Code cleanup - Improve script logging - Increase Diagrammer.Microsoft.AD module version requirements +- Change charts default font to Segoe Ui +- Improved Forest diagram ### Fixed -- Improve error handling on Forest Diagram section +- Improve error handling on Forest diagram section - Fix issues with Diagrammer.Microsoft.AD module - Fix DNS section not getting data when there are unavailable DC servers - Fix [#154](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/154) diff --git a/Src/Private/Get-AbrADDNSInfrastructure.ps1 b/Src/Private/Get-AbrADDNSInfrastructure.ps1 index c8e1f39..96a09b5 100644 --- a/Src/Private/Get-AbrADDNSInfrastructure.ps1 +++ b/Src/Private/Get-AbrADDNSInfrastructure.ps1 @@ -280,7 +280,7 @@ function Get-AbrADDNSInfrastructure { BlankLine Paragraph { Text "Reference:" -Bold - Text "https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/forwarders-resolution-timeouts" + Text "https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/forwarders-resolution-timeouts" -Color blue } BlankLine } diff --git a/Src/Private/Get-AbrADDomain.ps1 b/Src/Private/Get-AbrADDomain.ps1 index b335cce..fe9ed32 100644 --- a/Src/Private/Get-AbrADDomain.ps1 +++ b/Src/Private/Get-AbrADDomain.ps1 @@ -90,7 +90,7 @@ function Get-AbrADDomain { BlankLine Paragraph { Text "Reference:" -Bold - Text "https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/managing-rid-pool-depletion/ba-p/399736" + Text "https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/managing-rid-pool-depletion/ba-p/399736" -Color blue } } } diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index 7630dd8..a157a64 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -29,6 +29,41 @@ function Get-AbrADDomainController { } process { + try { + $OutObj = @() + $inObj = [ordered] @{ + 'Domain Controller' = $DomainController.Count + 'Global Catalog' = $GC.Count + } + $OutObj += [pscustomobject]$inobj + + $TableParams = @{ + Name = "Domain Controller Counts - $($Domain.ToString().ToUpper())" + List = $true + ColumnWidths = 40, 60 + } + if ($Report.ShowTableCaptions) { + $TableParams['Caption'] = "- $($TableParams.Name)" + } + if ($Options.EnableCharts) { + try { + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' + + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'DomainControllerObject' -XField 'Name' -YField 'value' -ChartLegendName 'Category' -ChartTitleName 'DomainControllerObject' -ChartTitleText 'DC vs GC Distribution' -ReversePalette $True + + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Count Chart)" + } + } + if ($OutObj) { + if ($chartFileItem) { + Image -Text 'Domain Controller Object - Diagram' -Align 'Center' -Percent 100 -Base64 $chartFileItem + } + $OutObj | Table @TableParams + } + } catch { + Write-PScriboMessage -IsWarning $($_.Exception.Message) + } if ($InfoLevel.Domain -eq 1) { try { $OutObj = @() @@ -36,7 +71,7 @@ function Get-AbrADDomainController { if (Test-Connection -ComputerName $DC -Quiet -Count 2) { $DCInfo = Invoke-Command -Session $TempPssSession { Get-ADDomainController -Identity $using:DC -Server $using:DC } $DCPssSession = New-PSSession $DC -Credential $Credential -Authentication $Options.PSDefaultAuthentication -Name 'DCNetSettings' - $DCNetSettings = try { Invoke-Command -Session $DCPssSession { Get-NetIPAddress } } catch { Out-Null } + $DCNetSettings = try { Invoke-Command -Session $DCPssSession { Get-NetIPAddress } } catch { Write-PScriboMessage -IsWarning "Unable to get $DC network interfaces information" } Remove-PSSession -Session $DCPssSession try { $inObj = [ordered] @{ @@ -184,6 +219,7 @@ function Get-AbrADDomainController { } 'Global Catalog' = ConvertTo-TextYN $DCInfo.IsGlobalCatalog 'Read Only' = ConvertTo-TextYN $DCInfo.IsReadOnly + 'Operation Master Roles' = $DCInfo.OperationMasterRoles -join ', ' 'Location' = $DCComputerObject.Location 'Computer Object SID' = $DCComputerObject.SID "Operating System" = $DCInfo.OperatingSystem @@ -231,7 +267,7 @@ function Get-AbrADDomainController { $inObj = [ordered] @{ 'IPv4 Addresses' = Switch ([string]::IsNullOrEmpty((($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address))) { $true { "--" } - $false { ($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address -join "," } + $false { ($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1' }).IPv4Address -join ", " } default { "Unknown" } } 'IPv6 Addresses' = Switch ([string]::IsNullOrEmpty((($DCNetSettings | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -ne '::1' }).IPv6Address))) { @@ -262,7 +298,7 @@ function Get-AbrADDomainController { BlankLine Paragraph { Text "Best Practice:" -Bold - Text "On Domain Controllers with more than one NIC where each NIC is connected to separate Network, there's a possibility that the Host A DNS registration can occur for unwanted NICs. Avoid registering unwanted NICs in DNS on a multihomed domain controller" + Text "On Domain Controllers with more than one NIC where each NIC is connected to separate Network, there's a possibility that the Host A DNS registration can occur for unwanted NICs. Avoid registering unwanted NICs in DNS on a multihomed domain controller." } } } diff --git a/Src/Private/Get-AbrADForest.ps1 b/Src/Private/Get-AbrADForest.ps1 index 4a1ed50..fd6389a 100644 --- a/Src/Private/Get-AbrADForest.ps1 +++ b/Src/Private/Get-AbrADForest.ps1 @@ -72,7 +72,7 @@ function Get-AbrADForest { } if ($HealthCheck.Domain.Security) { - $OutObj | Where-Object { $_.'Anonymous Access (dsHeuristics)' -eq 'Enabled' } | Set-Style -Style Warning -Property 'Anonymous Access (dsHeuristics)' + $OutObj | Where-Object { $_.'Anonymous Access (dsHeuristics)' -eq 'Enabled' } | Set-Style -Style Critical -Property 'Anonymous Access (dsHeuristics)' $OutObj | Where-Object { $_.'Tombstone Lifetime (days)' -lt 180 } | Set-Style -Style Warning -Property 'Tombstone Lifetime (days)' } @@ -93,10 +93,10 @@ function Get-AbrADForest { Text "Best Practice:" -Bold Text "Anonymous Access to Active Directory forest data above the rootDSE level must be disabled." } + BlankLine Paragraph "Reference:" -Bold BlankLine Paragraph "https://www.stigviewer.com/stig/active_directory_forest/2016-02-19/finding/V-8555" -Color blue - BlankLine } if ($OutObj | Where-Object { $_.'Tombstone Lifetime (days)' -lt 180 }) { Paragraph { @@ -260,7 +260,7 @@ function Get-AbrADForest { Paragraph { Text "Reference:" -Bold BlankLine - Text "https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-ad-recycle-bin-understanding-implementing-best-practices-and/ba-p/396944" + Text "https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-ad-recycle-bin-understanding-implementing-best-practices-and/ba-p/396944" -Color blue } } diff --git a/Src/Private/Get-AbrADOU.ps1 b/Src/Private/Get-AbrADOU.ps1 index 36c2261..cc98e67 100644 --- a/Src/Private/Get-AbrADOU.ps1 +++ b/Src/Private/Get-AbrADOU.ps1 @@ -77,7 +77,7 @@ function Get-AbrADOU { 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" + 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) { @@ -104,7 +104,7 @@ function Get-AbrADOU { } } if ($OutObj) { - Section -ExcludeFromTOC -Style NOTOCHeading3 "GPO Blocked Inheritance" { + Section -ExcludeFromTOC -Style NOTOCHeading4 "GPO Blocked Inheritance" { if ($HealthCheck.Domain.GPO) { $OutObj | Set-Style -Style Warning } diff --git a/Src/Private/Get-AbrADSecurityAssessment.ps1 b/Src/Private/Get-AbrADSecurityAssessment.ps1 index 3196b2a..4594b82 100644 --- a/Src/Private/Get-AbrADSecurityAssessment.ps1 +++ b/Src/Private/Get-AbrADSecurityAssessment.ps1 @@ -85,7 +85,7 @@ function Get-AbrADSecurityAssessment { try { $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Category'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } - $chartFileItem = Get-ColumnChart -SampleData $sampleData -ChartName 'AccountSecurityAssessment' -XField 'Category' -YField 'Value' -ChartAreaName 'Account Security Assessment' -AxisXTitle 'Categories' -AxisYTitle 'Number of Users' -ChartTitleName 'AccountSecurityAssessment' -ChartTitleText 'Assessment' + $chartFileItem = Get-ColumnChart -SampleData $sampleData -ChartName 'AccountSecurityAssessment' -XField 'Category' -YField 'Value' -ChartAreaName 'Account Security Assessment' -AxisXTitle 'Categories' -AxisYTitle 'Number of Users' -ChartTitleName 'AccountSecurityAssessment' -ChartTitleText 'Assessment' -ReversePalette $True } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Account Security Assessment Chart)" } @@ -185,7 +185,7 @@ function Get-AbrADSecurityAssessment { Paragraph { Text "** Privileged accounts such as those belonging to any of the administrator groups must not be trusted for delegation. Allowing privileged accounts to be trusted for delegation provides a means for privilege escalation from a compromised system. Delegation of privileged accounts must be prohibited." Text "Reference: " - Text "https://www.stigviewer.com/stig/active_directory_domain/2017-12-15/finding/V-36435" + Text "https://www.stigviewer.com/stig/active_directory_domain/2017-12-15/finding/V-36435" -Color blue } } } @@ -304,7 +304,7 @@ function Get-AbrADSecurityAssessment { 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. " + 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." } } } diff --git a/Src/Private/Get-AbrADSite.ps1 b/Src/Private/Get-AbrADSite.ps1 index de9ed8d..70f2b5d 100644 --- a/Src/Private/Get-AbrADSite.ps1 +++ b/Src/Private/Get-AbrADSite.ps1 @@ -287,19 +287,19 @@ function Get-AbrADSite { try { $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Sites } catch { - Write-PScriboMessage -IsWarning "Site Inventory Diagram Graph: $($_.Exception.Message)" + Write-PScriboMessage -IsWarning "Site Topology Diagram Graph: $($_.Exception.Message)" } if ($Graph) { If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } - Section -Style Heading4 "Site Inventory Diagram." { - Image -Base64 $Graph -Text "Site Inventory Diagram" -Percent $ImagePrty -Align Center + Section -Style Heading4 "Site Topology Diagram." { + Image -Base64 $Graph -Text "Site Topology Diagram" -Percent $ImagePrty -Align Center Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 } BlankLine -Count 2 } } catch { - Write-PScriboMessage -IsWarning "Site Inventory Diagram Section: $($_.Exception.Message)" + Write-PScriboMessage -IsWarning "Site Topology Diagram Section: $($_.Exception.Message)" } } try { diff --git a/Src/Private/SharedUtilsFunctions.ps1 b/Src/Private/SharedUtilsFunctions.ps1 index 60fac85..27a7625 100644 --- a/Src/Private/SharedUtilsFunctions.ps1 +++ b/Src/Private/SharedUtilsFunctions.ps1 @@ -1968,14 +1968,29 @@ function Get-PieChart { [int] $Width = 600, [int] - $Height = 400 + $Height = 400, + [bool] + $ReversePalette = $false ) - $exampleChart = New-Chart -Name $ChartName -Width $Width -Height $Height + $AbrCustomPalette = @( + [System.Drawing.ColorTranslator]::FromHtml('#355780') + [System.Drawing.ColorTranslator]::FromHtml('#48678f') + [System.Drawing.ColorTranslator]::FromHtml('#5b789e') + [System.Drawing.ColorTranslator]::FromHtml('#6e89ae') + [System.Drawing.ColorTranslator]::FromHtml('#809bbe') + [System.Drawing.ColorTranslator]::FromHtml('#94acce') + [System.Drawing.ColorTranslator]::FromHtml('#a7bfde') + [System.Drawing.ColorTranslator]::FromHtml('#bbd1ee') + [System.Drawing.ColorTranslator]::FromHtml('#cfe4ff') + ) + + $exampleChart = New-Chart -Name $ChartName -Width $Width -Height $Height -BorderColor 'DarkBlue' -BorderStyle Dash -BorderWidth 1 $addChartAreaParams = @{ Chart = $exampleChart Name = 'exampleChartArea' + AxisXInterval = 1 } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru @@ -1985,8 +2000,9 @@ function Get-PieChart { Name = 'exampleChartSeries' XField = $XField YField = $YField - Palette = 'Blue' + CustomPalette = $AbrCustomPalette ColorPerDataPoint = $true + ReversePalette = $ReversePalette } $sampleData | Add-PieChartSeries @addChartSeriesParams @@ -2002,7 +2018,7 @@ function Get-PieChart { ChartArea = $exampleChartArea Name = $ChartTitleName Text = $ChartTitleText - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Segoe Ui', '12', [System.Drawing.FontStyle]::Bold) } Add-ChartTitle @addChartTitleParams @@ -2057,10 +2073,24 @@ function Get-ColumnChart { [int] $Width = 600, [int] - $Height = 400 + $Height = 400, + [bool] + $ReversePalette = $false + ) + + $AbrCustomPalette = @( + [System.Drawing.ColorTranslator]::FromHtml('#355780') + [System.Drawing.ColorTranslator]::FromHtml('#48678f') + [System.Drawing.ColorTranslator]::FromHtml('#5b789e') + [System.Drawing.ColorTranslator]::FromHtml('#6e89ae') + [System.Drawing.ColorTranslator]::FromHtml('#809bbe') + [System.Drawing.ColorTranslator]::FromHtml('#94acce') + [System.Drawing.ColorTranslator]::FromHtml('#a7bfde') + [System.Drawing.ColorTranslator]::FromHtml('#bbd1ee') + [System.Drawing.ColorTranslator]::FromHtml('#cfe4ff') ) - $exampleChart = New-Chart -Name $ChartName -Width $Width -Height $Height + $exampleChart = New-Chart -Name $ChartName -Width $Width -Height $Height -BorderColor 'DarkBlue' -BorderStyle Dash -BorderWidth 1 $addChartAreaParams = @{ Chart = $exampleChart @@ -2069,6 +2099,7 @@ function Get-ColumnChart { AxisYTitle = $AxisYTitle NoAxisXMajorGridLines = $true NoAxisYMajorGridLines = $true + AxisXInterval = 1 } $exampleChartArea = Add-ChartArea @addChartAreaParams -PassThru @@ -2078,8 +2109,9 @@ function Get-ColumnChart { Name = 'exampleChartSeries' XField = $XField YField = $YField - Palette = 'Blue' + CustomPalette = $AbrCustomPalette ColorPerDataPoint = $true + ReversePalette = $ReversePalette } $sampleData | Add-ColumnChartSeries @addChartSeriesParams @@ -2088,7 +2120,7 @@ function Get-ColumnChart { ChartArea = $exampleChartArea Name = $ChartTitleName Text = $ChartTitleText - Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Arial', '12', [System.Drawing.FontStyle]::Bold) + Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList @('Segoe Ui', '12', [System.Drawing.FontStyle]::Bold) } Add-ChartTitle @addChartTitleParams diff --git a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 index d007930..ae5b494 100644 --- a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 +++ b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 @@ -75,6 +75,13 @@ function Invoke-AsBuiltReport.Microsoft.AD { # Used to set values to TitleCase where required $script:TextInfo = (Get-Culture).TextInfo + if ($Global:BuitReportParams.EnableHealthCheck) { + Section -Style TOC -ExcludeFromTOC 'DISCLAIMER' { + Paragraph "The information contained in this report has been obtained through automation and observations. Opinions, recommendations and conclusions are disseminated using insight, knowledge, training and experience. This assessment was not intended to be exhaustive. However, we have done our best to capture the most relevant opportunities for improvement. It is expected that responsibility for the implementation of these recommendations will be reviewed and implemented by a person with the necessary knowledge, experience or expertise. In no event shall the author(s) be liable for damages of any kind (including, but not limited to, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use or inability to use these recommendations or the statements made in this documentation." + } + PageBreak + } + #---------------------------------------------------------------------------------------------# # Connection Section # #---------------------------------------------------------------------------------------------# From b47bcd2a90be91949887beb9129b0422832f5e57 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 16 May 2024 12:36:43 -0400 Subject: [PATCH 14/17] v0.8.1 improvements --- AsBuiltReport.Microsoft.AD.json | 4 +- CHANGELOG.md | 5 ++ README.md | 1 - Src/Private/Get-AbrADDomainController.ps1 | 12 ++-- Src/Private/Get-AbrADDomainObject.ps1 | 74 +++++++++------------ Src/Private/Get-AbrADForest.ps1 | 28 ++++---- Src/Private/Get-AbrADSecurityAssessment.ps1 | 12 ++-- Src/Private/Get-AbrADSite.ps1 | 28 ++++---- Src/Private/Get-AbrADTrust.ps1 | 2 +- Src/Private/SharedUtilsFunctions.ps1 | 17 ----- 10 files changed, 75 insertions(+), 108 deletions(-) diff --git a/AsBuiltReport.Microsoft.AD.json b/AsBuiltReport.Microsoft.AD.json index 8dee1b1..77eb6d8 100644 --- a/AsBuiltReport.Microsoft.AD.json +++ b/AsBuiltReport.Microsoft.AD.json @@ -1,6 +1,6 @@ { "Report": { - "Name": "Microsoft AD As Built Report", + "Name": "Microsoft Active Directory As Built Report", "Version": "1.0", "Status": "Released", "ShowCoverPageImage": true, @@ -11,8 +11,6 @@ "Options": { "ShowDefinitionInfo": false, "PSDefaultAuthentication": "Negotiate", - "EnableCharts": false, - "EnableDiagrams": true, "Exclude": { "Domains": [], "DCs": [] diff --git a/CHANGELOG.md b/CHANGELOG.md index 179677e..abfad27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix [#151](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/151) - Fix [#150](https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.AD/issues/150) +### Removed + +- Removed EnableDiagrams option +- Removed EnableCharts option + ## [0.8.0] - 2024-01-24 ### Added diff --git a/README.md b/README.md index fb766ee..72096a0 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,6 @@ The **Options** schema allows certain options within the report to be toggled on | Exclude.DCs | Array List | Empty | Allow to filter on AD Domain Controller Server FQDN. | | Include.Domains | Array List | Empty | Allow only a list of Active Directory Domain Controller FQDN to document. | | Include.DCs | Array List | Empty | Allow only a list of Active Directory Domain FQDN to document. | -| EnableDiagrams | true / false | true | Toggle to enable/disable of Infrastructure Diagrams | ### InfoLevel diff --git a/Src/Private/Get-AbrADDomainController.ps1 b/Src/Private/Get-AbrADDomainController.ps1 index a157a64..f0183e3 100644 --- a/Src/Private/Get-AbrADDomainController.ps1 +++ b/Src/Private/Get-AbrADDomainController.ps1 @@ -45,15 +45,13 @@ function Get-AbrADDomainController { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' + try { + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'DomainControllerObject' -XField 'Name' -YField 'value' -ChartLegendName 'Category' -ChartTitleName 'DomainControllerObject' -ChartTitleText 'DC vs GC Distribution' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'DomainControllerObject' -XField 'Name' -YField 'value' -ChartLegendName 'Category' -ChartTitleName 'DomainControllerObject' -ChartTitleText 'DC vs GC Distribution' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Count Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Domain Controller Count Chart)" } if ($OutObj) { if ($chartFileItem) { diff --git a/Src/Private/Get-AbrADDomainObject.ps1 b/Src/Private/Get-AbrADDomainObject.ps1 index 85f65cf..a36f989 100644 --- a/Src/Private/Get-AbrADDomainObject.ps1 +++ b/Src/Private/Get-AbrADDomainObject.ps1 @@ -66,17 +66,16 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { + try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'UsersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'UsersObject' -ChartTitleText 'User Objects' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'UsersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'UsersObject' -ChartTitleText 'User Objects' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (User Object Count Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (User Object Count Chart)" } + if ($OutObj) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Users' { if ($chartFileItem) { @@ -168,16 +167,14 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { + try { - $sampleData = $OutObj + $sampleData = $OutObj - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofUsersAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofUsersAccounts' -ChartTitleText 'Status of Users Accounts' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofUsersAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofUsersAccounts' -ChartTitleText 'Status of Users Accounts' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Users Accounts Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Users Accounts Chart)" } } if ($OutObj) { @@ -249,16 +246,14 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { + try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Name' + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Name' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'GroupCategoryObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'GroupCategoryObject' -ChartTitleText 'Group Categories' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'GroupCategoryObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'GroupCategoryObject' -ChartTitleText 'Group Categories' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Category Object Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Category Object Chart)" } if ($OutObj) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Groups Categories' { @@ -288,16 +283,14 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { + try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Name' + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Name' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'GroupCategoryObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'GroupScopesObject' -ChartTitleText 'Group Scopes' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'GroupCategoryObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'GroupScopesObject' -ChartTitleText 'Group Scopes' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Scopes Object Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Group Scopes Object Chart)" } if ($OutObj) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Groups Scopes' { @@ -544,15 +537,13 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' + try { + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Name'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } | Sort-Object -Property 'Category' - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'ComputersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'ComputersObject' -ChartTitleText 'Computers Count' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'ComputersObject' -XField 'Name' -YField 'Value' -ChartLegendName 'Category' -ChartTitleName 'ComputersObject' -ChartTitleText 'Computers Count' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Computers Object Count Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Computers Object Count Chart)" } if ($OutObj) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Computers' { @@ -621,17 +612,16 @@ function Get-AbrADDomainObject { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { - $sampleData = $OutObj + try { + $sampleData = $OutObj - $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofComputerAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofComputerAccounts' -ChartTitleText 'Status of Computers Accounts' -ReversePalette $True + $chartFileItem = Get-PieChart -SampleData $sampleData -ChartName 'StatusofComputerAccounts' -XField 'Category' -YField 'Total' -ChartLegendName 'Category' -ChartTitleName 'StatusofComputerAccounts' -ChartTitleText 'Status of Computers Accounts' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Computers Accounts Chart)" - } + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Status of Computers Accounts Chart)" } + if ($OutObj) { Section -Style Heading5 'Status of Computer Accounts' { if ($chartFileItem -and ($OutObj.'Total' | Measure-Object -Sum).Sum -ne 0) { diff --git a/Src/Private/Get-AbrADForest.ps1 b/Src/Private/Get-AbrADForest.ps1 index fd6389a..06ada87 100644 --- a/Src/Private/Get-AbrADForest.ps1 +++ b/Src/Private/Get-AbrADForest.ps1 @@ -105,25 +105,23 @@ function Get-AbrADForest { } } } - if ($Options.EnableDiagrams) { + try { try { - try { - $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Forest - } catch { - Write-PScriboMessage -IsWarning "Forest Diagram Graph: $($_.Exception.Message)" - } + $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Forest + } catch { + Write-PScriboMessage -IsWarning "Forest Diagram Graph: $($_.Exception.Message)" + } - if ($Graph) { - If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } - Section -Style Heading3 "Forest Diagram." { - Image -Base64 $Graph -Text "Forest Diagram" -Percent $ImagePrty -Align Center - Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 - } - BlankLine -Count 2 + if ($Graph) { + If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } + Section -Style Heading3 "Forest Diagram." { + Image -Base64 $Graph -Text "Forest Diagram" -Percent $ImagePrty -Align Center + Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 } - } catch { - Write-PScriboMessage -IsWarning "Forest Diagram Section: $($_.Exception.Message)" + BlankLine -Count 2 } + } catch { + Write-PScriboMessage -IsWarning "Forest Diagram Section: $($_.Exception.Message)" } } } catch { diff --git a/Src/Private/Get-AbrADSecurityAssessment.ps1 b/Src/Private/Get-AbrADSecurityAssessment.ps1 index 4594b82..ff7fac7 100644 --- a/Src/Private/Get-AbrADSecurityAssessment.ps1 +++ b/Src/Private/Get-AbrADSecurityAssessment.ps1 @@ -81,14 +81,12 @@ function Get-AbrADSecurityAssessment { if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } - if ($Options.EnableCharts) { - try { + try { - $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Category'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } - $chartFileItem = Get-ColumnChart -SampleData $sampleData -ChartName 'AccountSecurityAssessment' -XField 'Category' -YField 'Value' -ChartAreaName 'Account Security Assessment' -AxisXTitle 'Categories' -AxisYTitle 'Number of Users' -ChartTitleName 'AccountSecurityAssessment' -ChartTitleText 'Assessment' -ReversePalette $True - } catch { - Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Account Security Assessment Chart)" - } + $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Category'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } } + $chartFileItem = Get-ColumnChart -SampleData $sampleData -ChartName 'AccountSecurityAssessment' -XField 'Category' -YField 'Value' -ChartAreaName 'Account Security Assessment' -AxisXTitle 'Categories' -AxisYTitle 'Number of Users' -ChartTitleName 'AccountSecurityAssessment' -ChartTitleText 'Assessment' -ReversePalette $True + } catch { + Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Account Security Assessment Chart)" } if ($OutObj) { Section -ExcludeFromTOC -Style NOTOCHeading4 'Account Security Assessment' { diff --git a/Src/Private/Get-AbrADSite.ps1 b/Src/Private/Get-AbrADSite.ps1 index 70f2b5d..f3faa6e 100644 --- a/Src/Private/Get-AbrADSite.ps1 +++ b/Src/Private/Get-AbrADSite.ps1 @@ -282,25 +282,23 @@ function Get-AbrADSite { } catch { Write-PScriboMessage -IsWarning "$($_.Exception.Message) (Site Subnets)" } - if ($Options.EnableDiagrams) { + try { try { - try { - $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Sites - } catch { - Write-PScriboMessage -IsWarning "Site Topology Diagram Graph: $($_.Exception.Message)" - } + $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Sites + } catch { + Write-PScriboMessage -IsWarning "Site Topology Diagram Graph: $($_.Exception.Message)" + } - if ($Graph) { - If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } - Section -Style Heading4 "Site Topology Diagram." { - Image -Base64 $Graph -Text "Site Topology Diagram" -Percent $ImagePrty -Align Center - Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 - } - BlankLine -Count 2 + if ($Graph) { + If ((Get-DiaImagePercent -GraphObj $Graph).Width -gt 1500) { $ImagePrty = 10 } else { $ImagePrty = 50 } + Section -Style Heading4 "Site Topology Diagram." { + Image -Base64 $Graph -Text "Site Topology Diagram" -Percent $ImagePrty -Align Center + Paragraph "Image preview: Opens the image in a new tab to view it at full resolution." -Tabs 2 } - } catch { - Write-PScriboMessage -IsWarning "Site Topology Diagram Section: $($_.Exception.Message)" + BlankLine -Count 2 } + } catch { + Write-PScriboMessage -IsWarning "Site Topology Diagram Section: $($_.Exception.Message)" } try { $DomainDN = Invoke-Command -Session $TempPssSession { (Get-ADDomain -Identity (Get-ADForest | Select-Object -ExpandProperty RootDomain )).DistinguishedName } diff --git a/Src/Private/Get-AbrADTrust.ps1 b/Src/Private/Get-AbrADTrust.ps1 index 467809a..3a40591 100644 --- a/Src/Private/Get-AbrADTrust.ps1 +++ b/Src/Private/Get-AbrADTrust.ps1 @@ -102,7 +102,7 @@ function Get-AbrADTrust { } $TrustInfo | Table @TableParams } - if ($Options.EnableDiagrams -and ($Domain -eq $ADSystem.RootDomain)) { + if ($Domain -eq $ADSystem.RootDomain) { try { try { $Graph = New-ADDiagram -Target $System -Credential $Credential -Format base64 -Direction top-to-bottom -DiagramType Trusts diff --git a/Src/Private/SharedUtilsFunctions.ps1 b/Src/Private/SharedUtilsFunctions.ps1 index 27a7625..5a7aa46 100644 --- a/Src/Private/SharedUtilsFunctions.ps1 +++ b/Src/Private/SharedUtilsFunctions.ps1 @@ -1463,7 +1463,6 @@ function Get-CimData { ) $CimObject } - function ConvertFrom-DistinguishedName { <# .SYNOPSIS @@ -1568,7 +1567,6 @@ function ConvertFrom-DistinguishedName { $Distinguished } while ($true) { - #$dn = $dn -replace '^.+?,(?=CN|OU|DC)' $Distinguished = $Distinguished -replace '^.+?,(?=..=)' if ($Distinguished -match '^DC=') { break @@ -1576,23 +1574,11 @@ function ConvertFrom-DistinguishedName { $Distinguished } } elseif ($ToDC) { - #return [Regex]::Match($DistinguishedName, '(?=DC=)(.*\n?)(?<=.)').Value - # return [Regex]::Match($DistinguishedName, '.*?(DC=.*)').Value $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' if ($Value) { $Value } - #return [Regex]::Match($DistinguishedName, 'CN=.*?(DC=.*)').Groups[1].Value } elseif ($ToLastName) { - # Would be best if it worked, but there is too many edge cases so hand splits seems to be the best solution - # Feel free to change it back to regex if you know how ;) - <# https://stackoverflow.com/questions/51761894/regex-extract-ou-from-distinguished-name - $Regex = "^(?:(?CN=(?.*?)),)?(?(?:(?(?:CN|OU).*?),)?(?(?:DC=.*)+))$" - $Found = $Distinguished -match $Regex - if ($Found) { - $Matches.name - } - #> $NewDN = $Distinguished -split ",DC=" if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" @@ -1608,13 +1594,10 @@ function ConvertFrom-DistinguishedName { } } else { $Regex = '^CN=(?.+?)(?(?:(?:OU|CN).+?(?DC.+?))$' - #$Output = foreach ($_ in $Distinguished) { $Found = $Distinguished -match $Regex if ($Found) { $Matches.cn } - #} - #$Output.cn } } } From d686783fd801ee762da8f43e283a943a8ddfeafb Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 16 May 2024 13:35:20 -0400 Subject: [PATCH 15/17] Updated release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abfad27..a043bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.8.1] - Unreleased +## [0.8.1] - 2024-05-16 ### Added From 02b299f9dc9010aa22e430e9ddae4dc2986db6d0 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 16 May 2024 13:57:01 -0400 Subject: [PATCH 16/17] Fix CodeQL #41 --- AsBuiltReport.Microsoft.AD.Style.ps1 | 2 +- Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AsBuiltReport.Microsoft.AD.Style.ps1 b/AsBuiltReport.Microsoft.AD.Style.ps1 index 3cb7004..b3c94e4 100644 --- a/AsBuiltReport.Microsoft.AD.Style.ps1 +++ b/AsBuiltReport.Microsoft.AD.Style.ps1 @@ -101,7 +101,7 @@ Table -Name 'Cover Page' -List -Style Borderless -Width 0 -Hashtable ([Ordered] }) PageBreak -if ($Global:BuitReportParams.EnableHealthCheck) { +if ($Healthcheck) { Section -Style TOC -ExcludeFromTOC 'DISCLAIMER' { Paragraph "The information contained in this report has been obtained through automation and observations. Opinions, recommendations and conclusions are disseminated using insight, knowledge, training and experience. This assessment was not intended to be exhaustive. However, we have done our best to capture the most relevant opportunities for improvement. It is expected that responsibility for the implementation of these recommendations will be reviewed and implemented by a person with the necessary knowledge, experience or expertise. In no event shall the author(s) be liable for damages of any kind (including, but not limited to, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use or inability to use these recommendations or the statements made in this documentation." } diff --git a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 index ae5b494..3fad7c0 100644 --- a/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 +++ b/Src/Public/Invoke-AsBuiltReport.Microsoft.AD.ps1 @@ -75,7 +75,7 @@ function Invoke-AsBuiltReport.Microsoft.AD { # Used to set values to TitleCase where required $script:TextInfo = (Get-Culture).TextInfo - if ($Global:BuitReportParams.EnableHealthCheck) { + if ($Healthcheck) { Section -Style TOC -ExcludeFromTOC 'DISCLAIMER' { Paragraph "The information contained in this report has been obtained through automation and observations. Opinions, recommendations and conclusions are disseminated using insight, knowledge, training and experience. This assessment was not intended to be exhaustive. However, we have done our best to capture the most relevant opportunities for improvement. It is expected that responsibility for the implementation of these recommendations will be reviewed and implemented by a person with the necessary knowledge, experience or expertise. In no event shall the author(s) be liable for damages of any kind (including, but not limited to, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use or inability to use these recommendations or the statements made in this documentation." } From 17ccfbbd1a8d0ce8f16c9b97eb85924dccc93771 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 16 May 2024 14:09:12 -0400 Subject: [PATCH 17/17] Update Sample Report --- .../Sample Microsoft AD As Built Report.html | 1887 ++++++++++++----- 1 file changed, 1359 insertions(+), 528 deletions(-) diff --git a/Samples/Sample Microsoft AD As Built Report.html b/Samples/Sample Microsoft AD As Built Report.html index f3061a1..83ce0ea 100644 --- a/Samples/Sample Microsoft AD As Built Report.html +++ b/Samples/Sample Microsoft AD As Built Report.html @@ -1,6 +1,6 @@  -Microsoft AD As Built Report +Microsoft Active Directory As Built Report