-
-
Save stares/cb9b3b14df0aa97e5f391404ce9b69a0 to your computer and use it in GitHub Desktop.
The script to perform AD assessment including ADFS, ADSync checks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -Version 3.0 | |
#Requires -Modules ActiveDirectory, GroupPolicy, DnsServer | |
<# | |
Author : Nitish Kumar | |
Performs Active Directory Forest Assessment | |
version 1.0 | 06/06/2023 Initial version | |
version 1.1 | 15/06/2023 Covered most areas though error proofing and dependency over wsman still remains | |
version 1.2 | 16/06/2023 Number of small fixes included wrong calulations on empty groups | |
The script is kept as much modular as possible so that functions can be modified or added without altering the entire script | |
It should be run as administrator and preferably Enterprise Administrator to get complete data. Its advised to run in demonstration environment to be sure first | |
Disclaimer: This script is designed to only read data from the domain and should not cause any problems or change configurations but author do not claim to be responsible for any issues. Do due dilligence before running in the production environment | |
#> | |
<# | |
.SYNOPSIS | |
Start-ADAssessment.ps1 - Perform Active Directory assessment and generate a report. | |
.DESCRIPTION | |
This script performs an assessment of Active Directory (AD) environment and generates a report with information | |
about domain controllers, replication status, DNS configuration, group policies, and more. | |
.NOTES | |
- This script requires elevated privileges and should be run as an administrator. | |
- Ensure that the required PowerShell modules and dependencies are installed (e.g., RSAT tools). | |
.EXAMPLE | |
Running the script would present a menu and would pick current forest or domain accordingly. | |
- Performs an assessment of the "example.com" domain and generates the report at the specified path. | |
#> | |
Import-Module ActiveDirectory | |
Import-Module GroupPolicy | |
Import-Module DnsServer | |
if (Get-Module -ListAvailable -Name DHCPServer) { | |
Import-Module DHCPServer | |
$DHCPFlag = $true | |
} | |
else { | |
$DHCPFlag = $false | |
} | |
# Output formating options | |
$logopath = "https://camo.githubusercontent.com/239d9de795c471d44ad89783ec7dc03a76f5c0d60d00e457c181b6e95c6950b6/68747470733a2f2f6e69746973686b756d61722e66696c65732e776f726470726573732e636f6d2f323032322f31302f63726f707065642d696d675f32303232303732335f3039343534372d72656d6f766562672d707265766965772e706e67" | |
$ReportPath1 = "$env:USERPROFILE\desktop\ADReport_$(get-date -Uformat "%Y%m%d-%H%M%S").html" | |
$CopyRightInfo = " @Copyright Nitish Kumar <a href='https://github.com/laymanstake'>Visit nitishkumar.net</a>" | |
[bool]$forestcheck = $false | |
$logpath = "$env:USERPROFILE\desktop\ADReport_$(get-date -Uformat "%Y%m%d-%H%M%S").txt" | |
# CSS codes to format the report | |
$header = @" | |
<style> | |
body { background-color: #b9d7f7; } | |
h1 { font-family: Arial, Helvetica, sans-serif; color: #e68a00; font-size: 28px; } | |
h2 { font-family: Arial, Helvetica, sans-serif; color: #000099; font-size: 16px; } | |
table { font-size: 12px; border: 1px; font-family: Arial, Helvetica, sans-serif; } | |
td { padding: 4px; margin: 0px; border: 1; } | |
th { background: #395870; background: linear-gradient(#49708f, #293f50); color: #fff; font-size: 11px; text-transform: uppercase; padding: 10px 15px; vertical-align: middle; } | |
tbody tr:nth-child(even) { background: #f0f0f2; } | |
CreationDate { font-family: Arial, Helvetica, sans-serif; color: #ff3300; font-size: 12px; } | |
</style> | |
"@ | |
If ($logopath) { | |
$header = $header + "<img src=$logopath alt='Company logo' width='150' height='150' align='right'>" | |
} | |
# Number of functions to get the details of the environment | |
# This function creates log entries for the major steps in the script. | |
function Write-Log { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$logtext, | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$logpath | |
) | |
$Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss") | |
$LogMessage = "$Stamp : $logtext" | |
Add-content $logpath -value $LogMessage | |
} | |
# This function retrieves detailed information about trust relationships in the Active Directory domain, including trust type and direction. | |
Function Get-ADTrustDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
if ($Credential) { | |
try { | |
$trusts = Get-ADTrust -Filter * -Server $DomainName -Credential $Credential -Properties Created, Modified, ForestTransitive | Select-Object Name, Source, Target, Created, Modified, Direction, TrustType, Intraforest, ForestTransitive | |
} | |
catch { | |
Write-Log -logtext "Could get trust details with credntials: $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
else { | |
try { | |
$trusts = Get-ADTrust -Filter * -Server $DomainName -Properties Created, Modified, ForestTransitive | Select-Object Name, Source, Target, Created, Modified, Direction, TrustType, Intraforest, ForestTransitive | |
} | |
catch { | |
Write-Log -logtext "Could get trust details without credentials: $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
$TrustDetails = @() | |
ForEach ($trust in $trusts) { | |
$stale = $false | |
if ($trust.TrustType -eq "External" -and $trust.Direction -eq "Bidirectional") { | |
try { | |
if ($Credential) { | |
$null = Get-ADDomain -Identity $trust.Target -Server $trust.Target -Credential $Credential -ErrorAction SilentlyContinue | |
} | |
else { | |
$null = Get-ADDomain -Identity $trust.Target -Server $trust.Target -ErrorAction SilentlyContinue | |
} | |
} | |
catch { | |
$stale = $true | |
} | |
} | |
$TrustDetails += [PSCustomObject]@{ | |
TrustName = $trust.Name | |
CreationDate = $trust.Created | |
ModificationDate = $trust.Modified | |
TrustSource = $trust.Source | |
TrustTarget = $trust.Target | |
TrustDirection = $trust.Direction | |
TrustType = $trust.TrustType | |
Intraforest = $trust.Intraforest | |
Foresttransitive = $trust.ForestTransitive | |
Stale = $stale | |
} | |
} | |
Return $TrustDetails | |
} | |
# This function gathers information about Active Directory Federation Services (ADFS), including ADFS\ ADSync servers, certificates, and endpoints. | |
Function Get-ADFSDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$adfsServers = @() | |
$aadconnectServers = @() | |
$ADFSServerDetails = @() | |
$AADCServerDetails = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$i = 0 | |
Get-ADComputer -Filter { OperatingSystem -like "*Server*" } -Server $PDC -Credential $Credential | | |
ForEach-Object { | |
$computer = $_.Name | |
try { | |
$service = ((Get-Service -ComputerName $computer -Name adfssrv -ErrorAction SilentlyContinue).Name , (Get-Service -ComputerName $computer -Name adsync -ErrorAction SilentlyContinue).Name ) | |
} | |
catch { | |
Write-Log -logtext "Could get Service details from $computer : $($_.Exception.Message)" -logpath $logpath | |
} | |
if ($service[0] -eq "adfssrv") { | |
$adfsServers += $computer | |
} | |
if ($service[1] -eq "adsync" ) { | |
$aadconnectServers += $computer | |
} | |
$i++ | |
if (($i % 10) -eq 1) { | |
$message = "$i servers checked so far." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
} | |
foreach ($server in $adfsServers) { | |
try { | |
if (Test-WSMan -ComputerName $server -ErrorAction SilentlyContinue) { | |
$ADFSproperties = invoke-command -ComputerName $server -ScriptBlock { import-module ADFS; Get-ADFSSyncProperties; (Get-ADFSProperties).Identifier; (Get-AdfsCertificate | Select-Object @{l = "certificate"; e = { "$($_.certificateType), $($_.Certificate.NotAfter), $($_.thumbprint)" } }).certificate } -Credential $Credential | |
} | |
} | |
catch { | |
Write-Log -logtext "ADFS Server - PS remoting NOT supported on $server : $($_.Exception.Message)" -logpath $logpath | |
} | |
if (($ADFSproperties[0]).Role -eq "PrimaryComputer") { | |
$isMaster = $true | |
} | |
else { | |
$isMaster = $false | |
} | |
$serverInfo = [PSCustomObject]@{ | |
ServerName = $server | |
OperatingSystem = (Get-ADComputer $server -server $PDC -Credential $Credential -properties OperatingSystem).OperatingSystem | |
IsMaster = $isMaster | |
ADFSName = $ADFSproperties[1] | |
Certificate = ($ADFSproperties[2], $ADFSproperties[3], $ADFSproperties[4]) -join "`n" | |
} | |
$ADFSServerDetails += $serverInfo | |
} | |
foreach ($server in $aadconnectServers) { | |
try { | |
$InstallPath = ((([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $server)).OpenSubKey('SOFTWARE\Microsoft\Azure AD Connect')).GetValue('Wizardpath')) -replace "\\", "\\" | |
$null = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $server)).Close(); | |
} | |
catch { | |
Write-Log -logtext "ADSync Server - Could not open remote regitry on $server : $($_.Exception.Message)" -logpath $logpath | |
} | |
if (Test-WSMan -ComputerName $server -ErrorAction SilentlyContinue) { | |
$ADSyncVersion = (Get-CimInstance -ClassName Cim_DataFile -ComputerName $server -Filter "Name='$InstallPath'").Version | |
} | |
else { | |
$ADSyncVersion = "Access denied" | |
Write-Log -logtext "ADSync Server - PS remoting NOT supported on $server : $($_.Exception.Message)" -logpath $logpath | |
} | |
$Info = [PSCustomObject]@{ | |
ServerName = $server | |
OperatingSystem = (Get-ADComputer $server -server $PDC -Credential $Credential -properties OperatingSystem).OperatingSystem | |
ADSyncVersion = $ADSyncVersion | |
IsActive = (Get-Service -ComputerName $Server -Name ADSync -ErrorAction SilentlyContinue).Status -eq "Running" | |
} | |
$AADCServerDetails += $Info | |
} | |
return $AADCServerDetails, $ADFSServerDetails | |
} | |
# This function collects information about certificate authorities. | |
Function Get-PKIDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$ForestName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PKIDetails = New-Object psobject | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $ForestName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$PKI = Get-ADObject -Filter { objectClass -eq "pKIEnrollmentService" } -Server $PDC -Credential $Credential -SearchBase "CN=Enrollment Services,CN=Public Key Services,CN=Services,$((Get-ADRootDSE).ConfigurationNamingContext)" -Properties DisplayName, DnsHostName | Select-Object DisplayName, DnsHostName, @{l = "OperatingSystem"; e = { (Get-ADComputer ($_.DNShostname -replace ".$ForestName") -Properties OperatingSystem -server $PDC -Credential $Credential).OperatingSystem } }, @{l = "IPv4Address"; e = { ([System.Net.Dns]::GetHostAddresses($_.DnsHostName) | Where-Object { $_.AddressFamily -eq "InterNetwork" }).IPAddressToString -join "`n" } } | |
If ($PKI) { | |
$PKIDetails = $PKI | |
try { | |
if ( Test-WSMan -ComputerName $PKI.DnsHostName -ErrorAction SilentlyContinue) { | |
$cert = invoke-command -ComputerName $PKI.DnsHostName -Credential $Credential -ScriptBlock { Get-ChildItem -Path cert:\LocalMachine\my | Where-Object { $_.issuer -eq $_.Subject } } | |
Add-Member -inputObject $PKIDetails -memberType NoteProperty -name "SecureHashAlgo" -value $cert.SignatureAlgorithm.FriendlyName | |
} | |
} | |
catch { | |
Write-Log -logtext "PKI Server - WinRM access denied, can't obtain SHA information from $server : $($_.Exception.Message)" -logpath $logpath | |
Add-Member -inputObject $PKIDetails -memberType NoteProperty -name "SecureHashAlgo" -value "UNKNOWN" | |
} | |
} | |
Return $PKIDetails | |
} | |
# This function retrieves detailed information about the Active Directory DNS configuration. | |
Function Get-ADDNSDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$DNSServerDetails = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
try { | |
$DNSServers = (Get-ADDomainController -Filter * -server $PDC -Credential $Credential) | Where-Object { Get-WmiObject -Class Win32_serverfeature -ComputerName $_.Name -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "DNS Server" } } | Select-Object Name, IPv4Address | |
} | |
catch { | |
Write-Log -logtext "Failed to get DNS servers list as one or more DC denied service details access : $($_.Exception.Message)" -logpath $logpath | |
} | |
ForEach ($DNSServer in $DNSServers) { | |
try { | |
$Scavenging = Get-DnsServerScavenging -ComputerName $DNSServer.Name -ErrorAction SilentlyContinue | |
$LastScavengeTime = $Scavenging.LastScavengeTime | |
if ($LastScavengeTime) { | |
$ScanvengingState = $true # This is a workaround since for corner cases, scanvenging would not be enabled for server but specific zones only and tedius to show that in summary table | |
} | |
else { | |
$ScanvengingState = $false | |
$LastScavengeTime = "" | |
} | |
} | |
catch { | |
Write-Log -logtext "Could not get DNS Scanvenging information from DNS Server $($DNSServer.Name) : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$Forwarders = (Get-DnsServerForwarder -ComputerName $DNSServer.Name -ErrorAction SilentlyContinue).IPAddress | |
} | |
catch { | |
Write-Log -logtext "Could not get DNS Forwarder info from DNS Server $($DNSServer.Name) : $($_.Exception.Message)" -logpath $logpath | |
} | |
$DNSServerDetails += [PSCustomObject]@{ | |
ServerName = $DNSServer.Name | |
IPAddress = $DNSServer.IPv4Address | |
OperatingSystem = (Get-ADComputer $DNSServer.Name -Properties OperatingSystem -Server $PDC -Credential $Credential).OperatingSystem | |
Forwarders = $Forwarders -join "`n" | |
ScanvengingState = $ScanvengingState | |
LastScavengeTime = $LastScavengeTime | |
} | |
} | |
return $DNSServerDetails | |
} | |
# This function provides detailed information about Active Directory DNS zones, including zone properties, zone transfers, and DNS server settings. | |
Function Get-ADDNSZoneDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$DNSServerZoneDetails = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$DNSZones = Get-DnsServerZone -ComputerName $PDC | Where-Object { -Not $_.IsReverseLookupZone } | Select-Object DistinguishedName, ZoneName, ZoneType, IsReadOnly, DynamicUpdate, IsSigned, IsWINSEnabled, ReplicationScope, MasterServers, SecureSecondaries, SecondaryServers | |
ForEach ($DNSZone in $DNSZones) { | |
If ($DNSZone.DistinguishedName) { | |
$Info = (Get-DnsServerZone -zoneName $DNSZone.ZoneName -ComputerName $PDC | Where-Object { -NOT $_.IsReverseLookupZone -AND $_.ZoneType -ne "Forwarder" }).Distinguishedname | ForEach-Object { Get-ADObject -Identity $_ -Server $PDC -Credential $Credential -Properties ProtectedFromAccidentalDeletion, Created } | |
try { | |
$Aging = Get-DnsServerZoneAging -ZoneName $DNSZone.ZoneName -ComputerName $PDC -ErrorAction SilentlyContinue | |
$ScanvengingState = $Aging.AgingEnabled | |
$RefreshInterval = $Aging.RefreshInterval | |
$NoRefreshInterval = $Aging.NoRefreshInterval | |
} | |
catch { | |
$ScanvengingState = "Unknown" | |
$RefreshInterval = "Unknown" | |
$NoRefreshInterval = "Unknown" | |
Write-Log -logtext "DNS Zone $($DNSZone.ZoneName) aging info not completed from $PDC : $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
Else { | |
$Info = [PSCustomObject]@{ | |
ProtectedFromAccidentalDeletion = $false | |
Created = "" | |
} | |
} | |
$ZoneInfo = New-Object PSObject | |
$ZoneInfo = $DNSZone | |
Add-Member -inputObject $ZoneInfo -memberType NoteProperty -name DNSServer -value $PDC | |
Add-Member -inputObject $ZoneInfo -memberType NoteProperty -name ProtectedFromDeletion -value $Info.ProtectedFromAccidentalDeletion | |
Add-Member -inputObject $ZoneInfo -memberType NoteProperty -name Created -value $Info.Created | |
Add-Member -inputObject $ZoneInfo -memberType NoteProperty -name ScanvengingState -value $ScanvengingState | |
Add-Member -inputObject $ZoneInfo -memberType NoteProperty -name RefreshInterval -value $RefreshInterval | |
Add-Member -inputObject $ZoneInfo -memberType NoteProperty -name NoRefreshInterval -value $NoRefreshInterval | |
$DNSServerZoneDetails += $ZoneInfo | Select-Object DNSServer, ZoneName, ProtectedFromDeletion, Created, ScanvengingState, RefreshInterval, NoRefreshInterval, ZoneType, IsReadOnly, DynamicUpdate, IsSigned, IsWINSEnabled, ReplicationScope, @{l = "MasterServers"; e = { $_.MasterServers -join "`n" } } , SecureSecondaries, @{l = "SecondaryServers"; e = { $_.SecondaryServers -join "`n" } } | |
} | |
return $DNSServerZoneDetails | |
} | |
# This function recursively retrieves all members of an Active Directory group, including nested groups and their members. | |
Function Get-ADGroupMemberRecursive { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$GroupName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$Domain = (Get-ADDomain -Identity $DomainName -Credential $Credential) | |
$PDC = $Domain.PDCEmulator | |
try { | |
$members = (Get-ADGroup -Identity $GroupName -Server $PDC -Credential $Credential -Properties Members).members | |
} | |
catch { | |
Write-Log -logtext "Failed to get member details for the group $GroupName : $($_.Exception.Message)" -logpath $logpath | |
} | |
$membersRecursive = @() | |
foreach ($member in $members) { | |
If ($member.Substring($member.IndexOf("DC=")) -eq $Domain.DistinguishedName) { | |
if ((Get-ADObject -identity $member -server $PDC -Credential $Credential).Objectclass -eq "group" ) { | |
$membersRecursive += Get-ADGroupMemberRecursive -GroupName $member -DomainName $Domain.DNSRoot -Credential $Credential | |
} | |
else { | |
$membersRecursive += Get-ADUser -identity $member -Server $PDC -Credential $Credential | Select-Object Name | |
} | |
} | |
} | |
return $membersRecursive | |
} | |
# This function identifies user accounts with the "AdminCount" attribute set, which can indicate privileged accounts and also which should not have admincount set. | |
Function Get-AdminCountDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$protectedGroups = (Get-ADGroup -Filter * -Server $PDC -Credential $Credential -properties adminCount | Where-Object { $_.adminCount -eq 1 }).Name | |
$ProtectedUsers = ($protectedGroups | ForEach-Object { Get-ADGroupMemberRecursive -GroupName $_ -DomainName $DomainName -Credential $Credential } | Sort-Object Name -Unique).Name | |
$UserWithAdminCount = (Get-ADUser -Filter * -Server $PDC -Credential $Credential -Properties AdminCount | Where-Object { $_.adminCount -eq 1 }).Name | |
$UndesiredAdminCount = (Compare-Object -ReferenceObject $UserWithAdminCount -DifferenceObject $ProtectedUsers | Where-Object { $_.SideIndicator -eq '<=' -AND $_.InputObject -ne "krbtgt" }).InputObject | |
$AdminCountDetails = [PSCustomObject]@{ | |
DomainName = $DomainName | |
ProtectedUsersCount = $ProtectedUsers.Count | |
UserWithAdminCount = $UserWithAdminCount.Count - 1 | |
UndesiredAdminCount = $UndesiredAdminCount.Count | |
UsersToClear = $UndesiredAdminCount -join "`n" | |
} | |
return $AdminCountDetails | |
} | |
# Return the details of DHCP Servers | |
Function Get-ADDHCPDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$configPartition = (Get-ADforest).PartitionsContainer.Replace("CN=Partitions,", "") | |
$AllDHCPServers = (Get-ADObject -SearchBase $configPartition -Filter "objectclass -eq 'dhcpclass' -AND Name -ne 'dhcproot'" -Server $PDC -Credential $Credential).Name | |
$DHCPDetails = @() | |
foreach ($dhcpserver in $AllDHCPServers) { | |
$ErrorActionPreference = "SilentlyContinue" | |
try { | |
$Allscopes = @(Get-DhcpServerv4Scope -ComputerName $dhcpserver -ErrorAction:SilentlyContinue) | |
$InactiveScopes = @($Allscopes | Where-Object { $_.State -eq 'Inactive' }) | |
$NoLeaseScopes = @() | |
foreach ($Scope in $Allscopes) { | |
$Leases = Get-DhcpServerv4Lease -ComputerName $dhcpserver -ScopeId $Scope.ScopeId | |
if ($Leases.Count -eq 0) { | |
$NoLeaseScopes += $Scope.ScopeID | |
} | |
} | |
try { | |
$OS = (Get-WmiObject win32_operatingSystem -ComputerName $dhcpserver -Property Caption).Caption | |
} | |
catch { | |
$OS = "Access denied" | |
Write-Log -logtext "Could not get operating system details for DHCP Server $dhcpserver : $($_.Exception.Message)" -logpath $logpath | |
} | |
$DHCPDetails += [PSCustomObject]@{ | |
ServerName = $dhcpserver | |
IPAddress = ([System.Net.Dns]::GetHostAddresses($dhcpserver) | Where-Object { $_.AddressFamily -eq "InterNetwork" }).IPAddressToString -join "`n" | |
OperatingSystem = $OS | |
ScopeCount = $Allscopes.count | |
InactiveScopeCount = $InactiveScopes.count | |
ScopeWithNoLease = $NoLeaseScopes -join "`n" | |
NoLeaseScopeCount = $NoLeaseScopes.count | |
} | |
} | |
catch { | |
Write-Log -logtext "Failed to get details from DHCP Server $dhcpserver : $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
return $DHCPDetails | |
} | |
# This function gathers information about DHCP servers in the Active Directory domain, including server configurations, scopes, and reservations. | |
Function Get-DHCPInventory { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
# Get all Authorized DCs from AD configuration | |
$DHCPs = Get-DhcpServerInDC | |
$Report = @() | |
$Reservations = @() | |
foreach ($dhcp in $DHCPs) { | |
if (Test-Connection -ComputerName $dhcp.DnsName -count 2) { | |
try { | |
$scopes = $null | |
$scopes = (Get-DhcpServerv4Scope -ComputerName $dhcp.DNSName -ErrorAction SilentlyContinue) | |
$Res = $scopes | ForEach-Object { Get-DHCPServerv4Lease -ComputerName $dhcp.DNSName -ScopeID $_.ScopeID } | Select-Object ScopeId, IPAddress, HostName, Description, ClientID, AddressState | |
ForEach ($Temp in $Res ) { | |
$Reservation = [PSCustomObject]@{ | |
ServerName = $dhcp.DNSName | |
ScopeID = $Temp.ScopeId | |
IPAddress = $Temp.IPAddress | |
HostName = $Temp.HostName | |
Description = $Temp.Description | |
ClientID = $Temp.ClientID | |
AddressState = $Temp.AddressState | |
} | select-object ServerName, ScopeID, IPAddress, HostName, Description, ClientID, AddressState | |
$Reservations += $Reservation | |
} | |
If ($null -ne $scopes) { | |
$GlobalDNSList = $null | |
#getting global DNS settings, in case scopes are configured to inherit these settings | |
$GlobalDNSList = (Get-DhcpServerv4OptionValue -OptionId 6 -ComputerName $dhcp.DNSName -ErrorAction:SilentlyContinue).Value | |
Try { $Option015 = [String](Get-DhcpServerv4OptionValue -OptionId 015 -ComputerName $dhcp.DNSName -ErrorAction:SilentlyContinue).value } | |
Catch { $Option015 = "" } | |
$scopes | ForEach-Object { | |
$row = "" | Select-Object Hostname, ScopeID, SubnetMask, Name, State, StartRange, EndRange, LeaseDuration, Description, DNS1, DNS2, DNS3, DNS4, DNS5, GDNS1, GDNS2, GDNS3, GDNS4, GDNS5, Router, DoGroupId, Option160, option015, Scopeoption015, Exclusions | |
Try { $DoGroupId = [String](Get-DhcpServerv4OptionValue -OptionId 234 -ScopeID $_.ScopeId -ComputerName $dhcp.DNSName -ErrorAction:SilentlyContinue).value } | |
Catch { $DoGroupId = "" } | |
Try { $Option160 = [String](Get-DhcpServerv4OptionValue -OptionId 160 -ScopeID $_.ScopeId -ComputerName $dhcp.DNSName -ErrorAction:SilentlyContinue).value } | |
Catch { $Option160 = "" } | |
Try { $ScopeOption015 = [String](Get-DhcpServerv4OptionValue -OptionId 015 -ScopeID $_.ScopeId -ComputerName $dhcp.DNSName -ErrorAction:SilentlyContinue).value } | |
Catch { $ScopeOption015 = "" } | |
$router = @() | |
Try { $router = (Get-DhcpServerv4OptionValue -ComputerName $dhcp.DNSName -OptionId 3 -ScopeID $_.ScopeId -ErrorAction:SilentlyContinue).Value } | |
Catch { $router = ("") } | |
$ScopeExclusions = @() | |
Try { $ScopeExclusions = Get-DhcpServerv4ExclusionRange -ComputerName $dhcp.DNSName -ScopeID $_.ScopeId -ErrorAction:SilentlyContinue } | |
Catch { $ScopeExclusions = ("") } | |
$Exclusions = "" | |
$z = 0 | |
If ($ScopeExclusions) { | |
ForEach ($ScopeExclusion in $ScopeExclusions) { | |
$z++ | |
$ExclusionValue = [String]$ScopeExclusion.StartRange + "-" + [String]$ScopeExclusion.EndRange | |
if ($z -ge 2) { $Exclusions = $Exclusions + "," + $ExclusionValue } else { $Exclusions = $ExclusionValue } | |
} | |
} | |
if ($router) { | |
$row.Router = $router[0] | |
} | |
else { | |
$row.Router = "" | |
} | |
$row.Hostname = $dhcp.DNSName | |
$row.ScopeID = $_.ScopeID | |
$row.SubnetMask = $_.SubnetMask | |
$row.Name = $_.Name | |
$row.State = $_.State | |
$row.StartRange = $_.StartRange | |
$row.EndRange = $_.EndRange | |
$row.LeaseDuration = $_.LeaseDuration | |
$row.Description = $_.Description | |
$row.DoGroupId = $DoGroupId | |
$row.Option160 = $Option160 | |
$row.Option015 = $Option015 | |
$row.ScopeOption015 = $ScopeOption015 | |
$row.Exclusions = $Exclusions | |
$ScopeDNSList = $null | |
$ScopeDNSList = (Get-DhcpServerv4OptionValue -OptionId 6 -ScopeID $_.ScopeId -ComputerName $dhcp.DNSName -ErrorAction:SilentlyContinue).Value | |
If (($null -eq $ScopeDNSList) -and ($null -ne $GlobalDNSList)) { | |
$row.GDNS1 = $GlobalDNSList[0] | |
$row.GDNS2 = $GlobalDNSList[1] | |
$row.GDNS3 = $GlobalDNSList[2] | |
$row.GDNS4 = $GlobalDNSList[3] | |
$row.GDNS5 = $GlobalDNSList[4] | |
} | |
ElseIf (($null -ne $ScopeDNSList) -and ($null -ne $GlobalDNSList)) { | |
$row.GDNS1 = $GlobalDNSList[0] | |
$row.GDNS2 = $GlobalDNSList[1] | |
$row.GDNS3 = $GlobalDNSList[2] | |
$row.GDNS4 = $GlobalDNSList[3] | |
$row.GDNS5 = $GlobalDNSList[4] | |
$row.DNS1 = $ScopeDNSList[0] | |
$row.DNS2 = $ScopeDNSList[1] | |
$row.DNS3 = $ScopeDNSList[2] | |
$row.DNS4 = $ScopeDNSList[3] | |
$row.DNS5 = $ScopeDNSList[4] | |
} | |
Else { | |
$row.DNS1 = $ScopeDNSList[0] | |
$row.DNS2 = $ScopeDNSList[1] | |
$row.DNS3 = $ScopeDNSList[2] | |
$row.DNS4 = $ScopeDNSList[3] | |
$row.DNS5 = $ScopeDNSList[4] | |
} | |
$row | |
$Report += $row | |
} | |
} | |
Else { | |
$row = "" | Select-Object Hostname, ScopeID, SubnetMask, Name, State, StartRange, EndRange, LeaseDuration, Description, DNS1, DNS2, DNS3, DNS4, DNS5, GDNS1, GDNS2, GDNS3, GDNS4, GDNS5, Router, DoGroupId, Option160, option015, Scopeoption015, Exclusions | |
$row.Hostname = $dhcp.DNSName | |
$Report += $row | |
} | |
} | |
catch { | |
Write-Log -logtext "Could not get scopes etc details for DHCP Server $($dhcp.DNSName) : $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
$message = "Working over DHCP Server $($dhcp.DNSName) related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
$Details = [pscustomobject] @{ | |
Inventory = $Report | |
Reservation = $Reservations | |
} | |
Return $Details | |
} | |
# This function identifies empty Organizational Units (OUs) in the Active Directory domain. | |
Function Get-EmptyOUDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$AllOUs = Get-ADOrganizationalUnit -Filter * -Server $PDC -Credential $Credential -Properties CanonicalName | |
$EmptyOUs = ($AllOUs | Where-Object { -not ( Get-ADObject -Filter * -SearchBase $_.Distinguishedname -SearchScope OneLevel -ResultSetSize 1 -Server $PDC -Credential $Credential) }).CanonicalName | |
$EmptyOUDetails = [PSCustomObject]@{ | |
Domain = $domainname | |
AllOUs = $AllOUs.count | |
EmptyOUs = $emptyOUs -join "`n" | |
EmptyOUCount = $emptyOUs.count | |
} | |
return $EmptyOUDetails | |
} | |
# This function identifies Active Directory objects that can be cleaned up, such as orphaned and lingering objects from given domain. | |
Function Get-ADObjectsToClean { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$ObjectsToClean = @() | |
$Domain = Get-ADDomain -Identity $DomainName -Credential $Credential | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$orphanedObj = Get-ADObject -Filter * -SearchBase "cn=LostAndFound,$($Domain.DistinguishedName)" -SearchScope OneLevel -Server $PDC -Credential $Credential | |
$lingConfReplObj = Get-ADObject -LDAPFilter "(cn=*\0ACNF:*)" -SearchBase $Domain.DistinguishedName -SearchScope SubTree -Server $PDC -Credential $Credential | |
$ObjectsToClean = [PSCustomObject]@{ | |
Domain = $domainname | |
OrphanedObjects = $orphanedObj.DistinguishedName -join "`n" | |
OrphanedObjectCount = $orphanedObj.Name.count | |
LingeringObjects = $lingConfReplObj.DistinguishedName -join "`n" | |
LingeringObjectCount = $lingConfReplObj.Name.count | |
} | |
return $ObjectsToClean | |
} | |
# This function summarizes Group Policy Objects (GPOs) in the Active Directory domain, including linked locations, and scope. | |
Function Get-ADGPOSummary { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$AllGPOs = Get-GPO -All -Domain $DomainName | |
$LinkedGPOs = @($AllGPOs | Where-Object { $_ | Get-GPOReport -ReportType XML -Domain $DomainName | Select-String -Pattern "<LinksTo>" -SimpleMatch } | Select-Object DisplayName, CreationTime, ModificationTime ) | |
$UnlinkedGPOs = @($AllGPOs | Where-Object { $_ | Get-GPOReport -ReportType XML -Domain $DomainName | Select-String -NotMatch "<LinksTo>" } | Select-Object DisplayName, CreationTime, ModificationTime ) | |
$DeactivatedGPOs = @($AllGPOs | Where-Object { $_.GPOStatus -eq "AllSettingsDisabled" } | Select-Object DisplayName, CreationTime, ModificationTime ) | |
$LinkedButDeactivatedGPOs = @() | |
If ($LinkedGPOs.count -ge 1 -AND $DeactivatedGPOs.Count -ge 1) { | |
$LinkedButDeactivatedGPOs = (Compare-Object -ReferenceObject $DeactivatedGPOs -DifferenceObject $LinkedGPOs -IncludeEqual | Where-Object { $_.SideIndicator -eq '==' } | Select-Object InputObject).InputObject | |
} | |
$GPOsAtRootLevel = (Get-GPInheritance -Target (Get-ADDomain -Identity $DomainName -credential $Credential).DistinguishedName).Gpolinks.DisplayName -join "`n" | |
$UnlinkedGPODetails = [PSCustomObject]@{ | |
Domain = $domainname | |
AllGPOs = $AllGPOs.count | |
GPOsAtRoot = $GPOsAtRootLevel | |
Unlinked = @($UnlinkedGPOs).DisplayName -join "`n" | |
UnlinkedCreationTime = @($UnlinkedGPOs).CreationTime -join "`n" | |
UnlinkedModificationTime = @($UnlinkedGPOs).ModificationTime -join "`n" | |
UnlinkedCount = @($UnlinkedGPOs).count | |
Deactivated = @($DeactivatedGPOs).DisplayName -join "`n" | |
DeactivatedCreationTime = @($DeactivatedGPOs).CreationTime -join "`n" | |
DeactivatedModificationTime = @($DeactivatedGPOs).ModificationTime -join "`n" | |
DeactivatedCount = @($DeactivatedGPOs).count | |
LinkedButDeactivated = @($LinkedButDeactivatedGPOs).DisplayName -join "`n" | |
LinkedButDeactivatedCount = @($LinkedButDeactivatedGPOs).Count | |
} | |
return $UnlinkedGPODetails | |
} | |
# This function provides an inventory of GPOs in the Active Directory domain, including their names, scope, wmi filters and applied locations. | |
Function Get-GPOInventory { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName | |
) | |
$GPOSummary = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$GPOs = Get-GPO -All -Domain $DomainName | |
$GPOs | ForEach-Object { | |
[xml]$Report = $_ | Get-GPOReport -ReportType XML -Domain $DomainName | |
$Permissions = Get-GPPermission -Name $Report.GPO.Name -All -DomainName $DomainName | Select-Object @{l = "Permission"; e = { "$($_.Trustee.Name), $($_.Trustee.SIDType), $($_.permission), Denied: $($_.Denied)" } } | |
$Links = $Report.GPO.LinksTo | |
try { | |
$wmifilterid = ($_.WmiFilter.Path -split '"')[1] | |
$wmiquery = ((Get-ADObject -Filter { objectClass -eq 'msWMI-Som' } -Server $PDC -Properties 'msWMI-Parm2' | where-object { $_.name -eq $wmifilterid })."msWMI-Parm2" -split "root\\CIMv2;")[1] | |
} | |
catch { | |
Write-Log -logtext "Erorr in getting WmiFilter $_.WmiFilter.Name query : $($_.Exception.Message)" -logpath $logpath | |
} | |
$GPOSummary += [pscustomobject]@{ | |
Domain = $DomainName | |
GPOName = $Report.GPO.Name | |
Creationtime = $_.CreationTime | |
ModificationTime = $_.ModificationTime | |
Link = $($Links.SOMPATH) -join "`n" | |
ComputerSettings = $Report.GPO.Computer.Enabled | |
UserSettings = $Report.GPO.User.Enabled | |
Permissions = $Permissions.Permission -join "`n" | |
WmiFilter = $_.WmiFilter.Name | |
WmiQuery = $wmiquery | |
} | |
} | |
return $GPOSummary | |
} | |
# This function retrieves the password policy settings configured in the Active Directory domain. | |
Function Get-ADPasswordPolicy { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$PwdPolicy = Get-ADDefaultDomainPasswordPolicy -Server $PDC -Credential $Credential | |
$DefaultPasswordPolicy = [PSCustomObject]@{ | |
DomainName = $DomainName | |
MinPwdAge = [string]($PwdPolicy.MinPasswordAge.days) + " Day(s)" | |
MaxPwdAge = [string]($PwdPolicy.MaxPasswordAge.days) + " Day(s)" | |
MinPwdLength = $PwdPolicy.MinPasswordLength | |
LockoutThreshold = $PwdPolicy.LockoutThreshold | |
LockoutDuration = $PwdPolicy.LockoutDuration | |
ComplexityEnabled = $PwdPolicy.ComplexityEnabled | |
ReversibleEncryption = $PwdPolicy.ReversibleEncryptionEnabled | |
PasswordHistoryCount = $PwdPolicy.PasswordHistoryCount | |
} | |
return $DefaultPasswordPolicy | |
} | |
# This function retrieves the settings of fine-grained password policies in the Active Directory domain. | |
Function Get-FineGrainedPasswordPolicy { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$FGPwdPolicyDetails = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$DomainFL = (Get-ADDomain -Identity $DomainName -Credential $Credential).DomainMode | |
if ( $DomainFL -in ("Windows2008Domain", "Windows2008R2Domain", "Windows2012Domain", "Windows2012R2Domain", "Windows2016Domain")) { | |
$FGPwdPolicy = Get-ADFineGrainedPasswordPolicy -Filter * -Server $PDC -Credential $Credential | |
ForEach ($FGPP in $FGPwdPolicy) { | |
$Obj = $FGPP.AppliesTo | ForEach-Object { Get-ADObject $_ -Server $PDC -Credential $Credential | Select-Object DistinguishedName , Name, ObjectClass } | |
$Users = $Obj | Where-Object { $_.ObjectClass -eq "User" } | |
$UserList = $Users | ForEach-Object { Get-ADUser -Identity $_.DistinguishedName -Server $PDC -Credential $Credential } | |
$Groups = $Obj | Where-Object { $_.ObjectClass -eq "Group" } | |
$GroupList = $Groups | ForEach-Object { Get-ADGroup -Identity $_.DistinguishedName -Server $PDC -Credential $Credential } | |
$FGPwdPolicyDetails += [PSCustomObject]@{ | |
DomainName = $DomainName | |
PolicyName = $FGPP.Name | |
MinPwdAge = [string]($FGPP.MinPasswordAge.days) + " Day(s)" | |
MaxPwdAge = [string]($FGPP.MaxPasswordAge.days) + " Day(s)" | |
MinPwdLength = $FGPP.MinPasswordLength | |
LockoutThreshold = $FGPP.LockoutThreshold | |
LockoutDuration = $FGPP.LockoutDuration | |
ComplexityEnabled = $FGPP.ComplexityEnabled | |
ReversibleEncryption = $FGPP.ReversibleEncryptionEnabled | |
PasswordHistoryCount = $FGPP.PasswordHistoryCount | |
AppliedonUsers = $UserList.Name -join "`n" | |
AppliedonGroups = $GroupList.Name -join "`n" | |
} | |
} | |
} | |
return $FGPwdPolicyDetails | |
} | |
# This function checks the status of SMBv1 (Server Message Block version 1) on the local or remote systems. | |
function Get-SMBv1Status { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$computername, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$result = @() | |
ForEach ($computer in $computername) { | |
try { | |
$smbv1ClientEnabled = $null | |
$smbv1ServerEnabled = $null | |
switch ((Get-WmiObject -Class Win32_OperatingSystem -ComputerName $Computer).Version) { | |
{ $_ -like "5*" } { $OSversion = "Windows Server 2003" } | |
{ $_ -like "6.0*" } { $OSversion = "Windows Server 2008" } | |
{ $_ -like "6.1*" } { $OSversion = "Windows Server 2008 R2" } | |
{ $_ -like "6.2*" } { $OSversion = "Windows Server 2012" } | |
{ $_ -like "6.3*" } { $OSversion = "Windows Server 2012 R2" } | |
{ $_ -like "10.0.14*" } { $OSversion = "Windows Server 2016" } | |
{ $_ -like "10.0.17*" } { $OSversion = "Windows Server 2019" } | |
{ $_ -like "10.0.19*" -OR $_ -like "10.0.2*" } { $OSversion = "Windows Server 2022" } | |
default { $OSversion = "Windows Server 2003" } | |
} | |
$smbv1ClientEnabled = (Get-Service -Name lanmanworkstation -ComputerName $Computer).DependentServices.name -contains "mrxsmb10" | |
If ($OSversion -in ("Windows Server 2003", "Windows Server 2008")) { | |
$ErrorActionPreference = "SilentlyContinue" | |
try { | |
$smbv1ServerEnabled = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $computer)).OpenSubKey('SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters').GetValue('EnableSMB') -eq 1 | |
} | |
Catch { | |
$smbv1ServerEnabled = "Unknown" | |
} | |
$null = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $computer)).Close(); | |
} | |
else { | |
try { | |
$smbv1ServerEnabled = Invoke-Command -ComputerName $Computer -ScriptBlock { (Get-SmbServerConfiguration).EnableSMB1Protocol } -Credential $credential | |
} | |
catch { | |
$smbv1ServerEnabled = "Unknown" | |
} | |
if ($null -eq $smbv1ServerEnabled) { | |
$smbv1ServerEnabled = "Unknown" | |
} | |
} | |
$result += [PSCustomObject]@{ | |
ComputerName = $Computer | |
OperatingSystem = $OSversion | |
SMBv1ClientEnabled = $smbv1ClientEnabled | |
SMBv1ServerEnabled = $smbv1ServerEnabled | |
} | |
} | |
catch { | |
Write-Log -logtext "Could not get SMBv1 status for $computer : $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
Return $result | |
} | |
# This function gathers detailed information about the Active Directory domain, including domain name, domain controllers, forest, and domain functional levels. | |
Function Get-ADDomainDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$DomainDetails = @() | |
$UndesiredFeatures = ("ADFS-Federation", "DHCP", "Telnet-Client", "WDS", "Web-Server", "Web-Application-Proxy", "FS-DFS-Namespace", "FS-DFS-Replication") | |
$dcs = Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential | |
$LowestOSversion = [version]::New(20, 0, 0) # Randomly picked unusually high version number | |
ForEach ($dc in $dcs) { | |
$version = $dc.OperatingSystemVersion -replace " ", "." -replace "\(", "" -replace "\)", "" -split "\." | |
$major = [int]$version[0] | |
$minor = [int]$version[1] | |
$build = [int]$version[2] | |
$osversion = [version]::New($major, $minor, $build) # Covert into a proper version | |
If ($osversion -lt $LowestOSversion ) { | |
$LowestOSversion = $osversion | |
} | |
} | |
switch ($LowestOSversion.ToString()) { | |
{ $_ -like "6.0*" } { $possibleDFL += "Windows Server 2008" } | |
{ $_ -like "6.1*" } { $possibleDFL += "Windows Server 2008 R2" } | |
{ $_ -like "6.2*" } { $possibleDFL += "Windows Server 2012" } | |
{ $_ -like "6.3*" } { $possibleDFL += "Windows Server 2012 R2" } | |
{ $_ -like "10.0.14*" } { $possibleDFL += "Windows Server 2016" } | |
{ $_ -like "10.0.17*" } { $possibleDFL += "Windows Server 2019" } | |
{ $_ -like "10.0.19*" -OR $_ -like "10.0.2*" } { $possibleDFL += "Windows Server 2022" } | |
default { $possibleDFL += "Windows Server 2003" } | |
} | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
if ((Get-ADObject -Server $PDC -Filter { name -like "SYSVOL*" } -Properties replPropertyMetaData -Credential $Credential).ReplPropertyMetadata.count -gt 0) { | |
$sysvolStatus = "DFSR" | |
} | |
else { | |
$sysvolStatus = "FSR" | |
} | |
# It needs WinRM being enabled on PDC | |
try { | |
if (Test-WSMan -ComputerName $PDC -ErrorAction SilentlyContinue) { | |
$FSR2DFSRStatus = invoke-command -ComputerName $PDC -ScriptBlock { ((dfsrmig.exe /GetGlobalState )[0].replace("'", "") -split ": ")[1] } -Credential $Credential | |
} | |
} | |
catch { | |
$FSR2DFSRStatus = "WinRM access denied on $PDC" | |
Write-Log -logtext "FSR2DFSR status - WinRM access denied on $PDC : $($_.Exception.Message)" -logpath $logpath | |
} | |
foreach ($dc in $dcs) { | |
if ( Test-Connection -ComputerName $dc -Count 1 -ErrorAction SilentlyContinue ) { | |
try { | |
$NLParamters = ((([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Services\Netlogon\Parameters\')).GetValueNames() | ForEach-Object { [PSCustomObject]@{ Parameter = $_; Value = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Services\Netlogon\Parameters\').GetValue($_) } } | ForEach-Object { "$($_.parameter), $($_.value)" }) -join "`n" | |
} | |
catch { | |
$NLParamters = "Reg not found" | |
Write-Log -logtext "Netlogon parameters not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$SSL2Client = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('Enabled') -eq 1 -AND ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('DisabledByDefault') -eq 0 | |
} | |
catch { | |
$SSL2Client = "Reg not found" | |
Write-Log -logtext "SSL 2.0 Client reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$SSL2Server = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server').GetValue('Enabled') -eq 1 -AND ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('DisabledByDefault') -eq 0 | |
} | |
catch { | |
$SSL2Server = "Reg not found" | |
Write-Log -logtext "SSL 2.0 Server reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$TLS10Client = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client').GetValue('Enabled') -eq 1 -AND ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('DisabledByDefault') -eq 0 | |
} | |
catch { | |
$TLS10Client = "Reg not found" | |
Write-Log -logtext "TLS 1.0 Client reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$TLS10Server = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server').GetValue('Enabled') -eq 1 -AND ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('DisabledByDefault') -eq 0 | |
} | |
catch { | |
$TLS10Server = "Reg not found" | |
Write-Log -logtext "TLS 1.0 Server reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$TLS11Client = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client').GetValue('Enabled') -eq 1 -AND ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('DisabledByDefault') -eq 0 | |
} | |
catch { | |
$TLS11Client = "Reg not found" | |
Write-Log -logtext "TLS 1.1 Client reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$TLS11Client = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server').GetValue('Enabled') -eq 1 -AND ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client').GetValue('DisabledByDefault') -eq 0 | |
} | |
catch { | |
$TLS11Client = "Reg not found" | |
Write-Log -logtext "TLS 1.1 Client reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$NTPServer = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Services\W32Time\Parameters').GetValue('NTPServer') | |
} | |
catch { | |
$NTPServer = "Reg not found" | |
Write-Log -logtext "NTP Server reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$NTPType = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).OpenSubKey('SYSTEM\CurrentControlSet\Services\W32Time\Parameters').GetValue('Type') | |
} | |
catch { | |
$NTPType = "Reg not found" | |
Write-Log -logtext "NTP Server Type reg key not found on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
$results = ($NLParamters, $SSL2Client, $SSL2Server, $TLS10Client, $TLS10Server, $TLS11Client, $TLS11Client, $NTPServer, $NTPType) | |
$null = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).Close(); | |
try { | |
$InstalledFeatures = (Get-WindowsFeature -ComputerName $dc -Credential $Credential | Where-Object Installed).Name | |
} | |
catch { | |
Write-Log -logtext "Failed to get installed features on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
if ($InstalledFeatures) { | |
$UndesiredFeature = (Compare-Object -ReferenceObject $UndesiredFeatures -DifferenceObject $InstalledFeatures -IncludeEqual | Where-Object { $_.SideIndicator -eq '==' } | Select-Object -ExpandProperty InputObject) | |
} | |
$SMBStatus = Get-SMBv1Status -computername $dc.Name -Credential $Credential | |
$DomainDetails += [PSCustomObject]@{ | |
Domain = $domain | |
DomainFunctionLevel = (Get-ADDomain -Identity $DomainName -Credential $Credential).DomainMode | |
PossibleDomainFunctionLevel = $possibleDFL | |
DCName = $dc.Name | |
Site = $dc.site | |
OSVersion = $dc.OperatingSystem | |
IPAddress = $dc.IPv4Address | |
FSMORoles = (Get-ADDomainController -Identity $dc -Server $PDC -Credential $Credential | Select-Object @{l = "FSMORoles"; e = { $_.OperationMasterRoles -join ", " } }).FSMORoles | |
Sysvol = $sysvolStatus | |
FSR2DFSR = $FSR2DFSRStatus | |
LAPS = $null -ne (Get-ADObject -LDAPFilter "(name=ms-Mcs-AdmPwd)" -Server $PDC -Credential $Credential) | |
NTPServer = ($Results[7] | Select-Object -Unique) -join "`n" | |
NTPType = $Results[8] | |
SMBv1Client = $SMBStatus.SMBv1ClientEnabled | |
SMBv1Server = $SMBStatus.SMBv1ServerEnabled | |
ADWSStatus = (Get-Service ADWS -computername $dc.Name -ErrorAction SilentlyContinue ).StartType | |
SSL2Client = $Results[1] | |
SSL2Server = $Results[2] | |
TLS1Client = $Results[3] | |
TLS1Server = $Results[4] | |
TLS11Client = $Results[5] | |
TLS11Server = $Results[6] | |
Firewall = (Get-Service -name MpsSvc -ComputerName $dc).Status | |
NetlogonParameter = $Results[0] | |
ReadOnly = $dc.IsReadOnly | |
UndesiredFeatures = $UndesiredFeature -join "`n" | |
} | |
} | |
} | |
$message = "Working over domain: $DomainName domain controller $($dc.Name) details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
return $DomainDetails | |
} | |
# This function provides detailed information about Active Directory sites, including site names, subnet assignments, and site links. | |
Function Get-ADSiteDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$SiteDetails = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
try { | |
$sites = Get-ADReplicationSite -Filter * -Server $PDC -Credential $Credential -Properties WhenCreated, WhenChanged, ProtectedFromAccidentalDeletion, Subnets | |
} | |
catch { | |
Write-Log -logtext "Failed to get replication sites : $($_.Exception.Message)" -logpath $logpath | |
} | |
foreach ($site in $sites) { | |
$dcs = @(Get-ADDomainController -Filter { Site -eq $site.Name } -Server $PDC -Credential $Credential) | |
if ($dcs.Count -eq 0) { | |
try { | |
$links = Get-ADReplicationSiteLink -Filter * -Server $PDC -Credential $Credential -Properties InterSiteTransportProtocol, replInterval, ProtectedFromAccidentalDeletion | Where-Object { $_.sitesIncluded -contains $site.DistinguishedName } | |
} | |
catch { | |
Write-Log -logtext "Failed to get replication site link from $PDC : $($_.Exception.Message)" -logpath $logpath | |
} | |
foreach ($link in $links) { | |
$SiteDetails += [pscustomobject]@{ | |
DomainName = $DomainName | |
SiteName = $site.Name | |
SiteCreated = $Site.WhenCreated | |
SiteModified = $Site.WhenChanged | |
Subnets = ($site.subnets | Get-ADReplicationSubnet -Server $PDC -Credential $Credential | Select-Object Name).Name -join "`n" | |
DCinSite = "No DCs in site" | |
SiteLink = $link.Name | |
SiteLinkType = $link.InterSiteTransportProtocol | |
SiteLinkCost = $link.Cost | |
ReplicationInterval = $link.replInterval | |
SiteProtectedFromAccidentalDeletion = $site.ProtectedFromAccidentalDeletion | |
LinkProtectedFromAccidentalDeletion = $link.ProtectedFromAccidentalDeletion | |
} | |
} | |
} | |
else { | |
foreach ($dc in $dcs) { | |
try { | |
$links = Get-ADReplicationSiteLink -Filter * -Server $PDC -Credential $Credential -Properties InterSiteTransportProtocol, replInterval, ProtectedFromAccidentalDeletion | Where-Object { $_.sitesIncluded -contains $site.DistinguishedName } | |
} | |
catch { | |
Write-Log -logtext "Failed to get replication site link from $($DC.Name) : $($_.Exception.Message)" -logpath $logpath | |
} | |
foreach ($link in $links) { | |
$SiteDetails += [pscustomobject]@{ | |
DomainName = $DomainName | |
SiteName = $site.Name | |
SiteCreated = $Site.WhenCreated | |
SiteModified = $Site.WhenChanged | |
Subnets = ($site.subnets | Get-ADReplicationSubnet -Server $PDC -Credential $Credential | Select-Object Name).Name -join "`n" | |
DCinSite = $dc.Name | |
SiteLink = $link.Name | |
SiteLinkType = $link.InterSiteTransportProtocol | |
SiteLinkCost = $link.Cost | |
ReplicationInterval = $link.replInterval | |
SiteProtectedFromAccidentalDeletion = $site.ProtectedFromAccidentalDeletion | |
LinkProtectedFromAccidentalDeletion = $link.ProtectedFromAccidentalDeletion | |
} | |
} | |
} | |
} | |
$message = "Working over domain: $DomainName site $($site.Name) details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
$SiteDetails = $SiteDetails | Select-Object DomainName, SiteName, SiteCreated, SiteModified, Subnets, SiteProtectedFromAccidentalDeletion, DCinSite, SiteLink, SiteLinkType, SiteLinkCost, ReplicationInterval, LinkProtectedFromAccidentalDeletion | Sort-Object DomainName, SiteLink | |
return $SiteDetails | |
} | |
# This function retrieves information about privileged groups in the Active Directory domain. | |
Function Get-PrivGroupDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$domainSID = (Get-ADDomain $DomainName -server $PDC -Credential $Credential -ErrorAction SilentlyContinue).domainSID.Value | |
$PrivGroupSIDs = @(@("Domain Admins", ($domainSID + "-512")), | |
@("Domain Guests", ($domainSID + "-514")), | |
@("Cert Publishers", ($domainSID + "-517")), | |
@("Group Policy Creator Owners", ($domainSID + "-520")), | |
@("Account Operators", "S-1-5-32-548"), | |
@("Server Operators", "S-1-5-32-549"), | |
@("Backup Operators", "S-1-5-32-551"), | |
@("Remote Desktop Users", "S-1-5-32-555")) | |
$PrivGroups = @() | |
ForEach ($groupSID in $PrivGroupSIDs ) { | |
$PrivGroups += [PSCustomObject]@{ | |
DomainName = $DomainName | |
OriginalGroupName = $groupSID[0] | |
GroupName = (Get-ADGroup -Server $PDC -Identity $GroupSID[1] -Credential $Credential).Name | |
MemberCount = @(Get-ADGroupMember -Server $PDC -Credential $Credential -Identity $GroupSID[1] -Recursive ).count | |
IsRenamed = $groupSID[0] -ne (Get-ADGroup -Server $PDC -Credential $Credential -Identity $GroupSID[1]).Name | |
} | |
} | |
return $PrivGroups | |
} | |
# This function retrieves detailed information about Active Directory groups. | |
Function Get-ADGroupDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$GroupDetails = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$AllGroups = Get-ADGroup -Filter { GroupCategory -eq "Security" } -Properties GroupScope -Server $PDC -Credential $Credential | |
$Filter = "(&(objectCategory=Group)(!member=*))" | |
$EmptyGroups = Get-ADGroup -LDAPFilter $Filter -Server $PDC -Credential $Credential -Properties GroupScope | |
$GroupDetails += [PSCustomObject]@{ | |
DomainName = $DomainName | |
GroupType = "Global" | |
GroupCount = ($AllGroups | Where-Object { $_.GroupScope -eq "Global" }).count | |
EmptyGroups = ($EmptyGroups | Where-Object { $_.GroupScope -eq "Global" }).Name -join "`n" | |
EmptyGroupCount = ($EmptyGroups | Where-Object { $_.GroupScope -eq "Global" }).count | |
} | |
$GroupDetails += [PSCustomObject]@{ | |
DomainName = $DomainName | |
GroupType = "DomainLocal" | |
GroupCount = ($AllGroups | Where-Object { $_.GroupScope -eq "DomainLocal" }).count | |
EmptyGroups = ($EmptyGroups | Where-Object { $_.GroupScope -eq "DomainLocal" }).Name -join "`n" | |
EmptyGroupCount = ($EmptyGroups | Where-Object { $_.GroupScope -eq "DomainLocal" }).count | |
} | |
$GroupDetails += [PSCustomObject]@{ | |
DomainName = $DomainName | |
GroupType = "Universal" | |
GroupCount = ($AllGroups | Where-Object { $_.GroupScope -eq "Universal" }).count | |
EmptyGroups = ($EmptyGroups | Where-Object { $_.GroupScope -eq "Universal" }).Name -join "`n" | |
EmptyGroupCount = ($EmptyGroups | Where-Object { $_.GroupScope -eq "Universal" }).count | |
} | |
return $GroupDetails | |
} | |
# This function gathers detailed information about Active Directory user accounts, including user properties and account status. | |
Function Get-ADUserDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$AllUsers = Get-ADUser -Filter * -Server $PDC -Properties SamAccountName, Enabled, whenCreated, PasswordLastSet, PasswordExpired, PasswordNeverExpires, PasswordNotRequired, AccountExpirationDate, LastLogonTimestamp, LockedOut | Select-object SamAccountName, Enabled, whenCreated, PasswordLastSet, PasswordExpired, PasswordNeverExpires, PasswordNotRequired, AccountExpirationDate, @{l = "Lastlogon"; e = { [DateTime]::FromFileTime($_.LastLogonTimestamp) } }, LockedOut | |
$UserDetails = [PSCustomObject]@{ | |
DomainName = $DomainName | |
Users = $AllUsers.count | |
RecentlyCreated = @($AllUsers | Where-Object { $_.whenCreated -ge (Get-Date).AddDays(-30) }).count | |
ChangeOnNextLogon = @($AllUsers | Where-Object { ($null -eq $_.PasswordLastSet) -and ($_.PasswordNotRequired -eq $false) }).count | |
PasswordExpired = @($AllUsers | Where-Object { $_.PasswordExpired -eq $true }).count | |
Disabled = @($AllUsers | Where-Object { $_.Enabled -eq $false }).count | |
LockedOut = @($AllUsers | Where-Object { $_.LockedOut -eq $true }).count | |
AccountExpired = @($AllUsers | Where-Object { $_.AccountExpirationDate -lt (Get-Date) -AND $null -ne $_.AccountExpirationDate }).count | |
Inactive = @($AllUsers | Where-Object { $_.LastLogon -lt (Get-Date).AddDays(-30) -AND $null -ne $_.LastLogon }).count | |
PasswordNeverExpires = @($AllUsers | Where-Object { $_.PasswordNeverExpires -eq $true }).count | |
} | |
return $UserDetails | |
} | |
# This function retrieves information about built-in user accounts in the Active Directory domain. | |
Function Get-BuiltInUserDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$domainSID = (Get-ADDomain $DomainName -server $PDC -Credential $Credential -ErrorAction SilentlyContinue).domainSID.Value | |
$BuiltInUserSIDs = @(@("Administrator", ($domainSID + "-500")), @("Guest", ($domainSID + "-501")), @("krbtgt", ($domainSID + "-502"))) | |
$BuiltInUsers = @() | |
ForEach ($UserSID in $BuiltInUserSIDs ) { | |
$User = Get-ADUser -Server $PDC -Credential $Credential -Identity $UserSID[1] -Properties SamAccountName, WhenCreated, LastLogonTimestamp, Enabled, LastBadPasswordAttempt, PasswordLastSet | select-Object SamAccountName, WhenCreated, @{l = "Lastlogon"; e = { [DateTime]::FromFileTime($_.LastLogonTimestamp) } }, Enabled, LastBadPasswordAttempt, PasswordLastSet | |
$BuiltInUsers += [PSCustomObject]@{ | |
DomainName = $DomainName | |
OriginalUserName = $UserSID[0] | |
UserName = $user.SamAccountName | |
Enabled = $user.Enabled | |
WhenCreated = $user.WhenCreated | |
LastLogonDate = $User.LastLogon | |
PasswordLastSet = $User.PasswordLastSet | |
LastBadPasswordAttempt = $User.LastBadPasswordAttempt | |
IsRenamed = $UserSID[0] -ne $user.SamAccountName | |
} | |
} | |
return $BuiltInUsers | |
} | |
# This function identifies Orphaned Foreign Security Principals for the given domain, be cautious as domain connectivity issues can flag false positive. | |
Function Get-OrphanedFSP { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$orphanedFSPs = @() | |
$Domain = Get-ADDomain -Identity $DomainName -Credential $Credential | |
$PDC = $Domain.PDCEmulator | |
$AllFSPs = Get-ADObject -Filter { ObjectClass -eq 'ForeignSecurityPrincipal' } -Server $PDC -Credential $Credential | |
<# NT AUTHORITY\INTERACTIVE, NT AUTHORITY\Authenticated Users, NT AUTHORITY\IUSR, NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS #> | |
$KnownFSPs = (("CN=S-1-5-4,CN=ForeignSecurityPrincipals," + $Domain), ("CN=S-1-5-11,CN=ForeignSecurityPrincipals," + $Domain), ("CN=S-1-5-17,CN=ForeignSecurityPrincipals," + $Domain), ("CN=S-1-5-9,CN=ForeignSecurityPrincipals," + $Domain)) | |
foreach ($FSP in $AllFSPs) { | |
Try { | |
$null = (New-Object System.Security.Principal.SecurityIdentifier($FSP.objectSid)).Translate([System.Security.Principal.NTAccount]) | |
} | |
Catch { | |
If (-NOT($FSp.DistinguishedName -in $KnownFSPs)) { | |
$OrphanedFSPs += $FSp.DistinguishedName | |
} | |
} | |
} | |
$OrphanedFSPDetails = [PSCustomObject]@{ | |
DomainName = $DomainName | |
OrphanedFSPs = $OrphanedFSPs -join "`n" | |
} | |
return $OrphanedFSPDetails | |
} | |
# This function gathers detailed information about servers in the Active Directory domain, including computer properties, operating system details, and stale info. | |
Function Get-DomainServerDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$DomainServerDetails = @() | |
$Today = Get-Date | |
$InactivePeriod = 90 | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$Servers = Get-ADComputer -Filter { OperatingSystem -Like "*Server*" } -Properties OperatingSystem, PasswordLastSet -Server $PDC -Credential $Credential | |
$OSs = $Servers | Group-Object OperatingSystem | Select-Object Name, Count | |
ForEach ($OS in $OSs) { | |
$DomainServerDetails += [PSCustomObject]@{ | |
DomainName = $DomainName | |
OSName = $OS.Name | |
Count = $OS.count | |
StaleCount_90d = ($Servers | Where-Object { $_.OperatingSystem -eq $OS.Name -AND $_.PasswordLastSet -lt $Today.AddDays( - ($InactivePeriod)) }).Name.Count | |
} | |
} | |
return $DomainServerDetails | |
} | |
# This function gathers detailed information about client computers in the Active Directory domain, including computer properties, operating system details, and stale info. | |
Function Get-DomainClientDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$DomainClientDetails = @() | |
$Today = Get-Date | |
$InactivePeriod = 90 | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$Workstations = Get-ADComputer -Filter { OperatingSystem -Notlike "*Server*" } -Properties OperatingSystem, PasswordLastSet -Server $PDC -Credential $Credential | |
$OSs = $Workstations | Group-Object OperatingSystem | Select-Object Name, Count | |
If ($OSs.count -gt 0) { | |
ForEach ($OS in $OSs) { | |
$DomainClientDetails += [PSCustomObject]@{ | |
DomainName = $DomainName | |
OSName = $OS.Name | |
Count = $OS.count | |
StaleCount_90d = ($Workstations | Where-Object { $_.OperatingSystem -eq $OS.Name -AND $_.PasswordLastSet -lt $Today.AddDays( - ($InactivePeriod)) }).Name.Count | |
} | |
} | |
} | |
return $DomainClientDetails | |
} | |
# The function performs various checks and assessments to identify unsecured configurations, potential security risks, and other security-related aspects. | |
Function Start-SecurityCheck { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$SecuritySettings = @() | |
$DCs = (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).hostname | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
ForEach ($DC in $DCs) { | |
if (Test-Connection -ComputerName $Dc -Count 4 -Quiet) { | |
try { | |
$results = ( | |
([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $DC)).OpenSubKey('System\CurrentControlSet\Control\Lsa').GetValue('LmCompatibilityLevel'), | |
([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $DC)).OpenSubKey('System\CurrentControlSet\Control\Lsa').GetValue('NoLMHash'), | |
([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $DC)).OpenSubKey('System\CurrentControlSet\Control\Lsa').GetValue('RestrictAnonymous'), | |
([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $DC)).OpenSubKey('System\CurrentControlSet\Services\NTDS\Parameters').GetValue('LDAPServerIntegrity'), | |
([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $DC)).OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System').GetValue('InactivityTimeoutSecs') | |
) | |
$null = ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $dc)).Close() | |
} | |
catch { | |
Write-Log -logtext "Could not check for security related registry keys on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
$results = ("", "", "", "", "") | |
} | |
if ($results) { | |
$NTLM = switch ($results[0]) { | |
5 { "Send NTLMv2 response only. Refuse LM & NTLM" } | |
4 { "Send NTLMv2 response only. Refuse LM" } | |
3 { "Send NTLMv2 response only" } | |
2 { "Send NTLM response only" } | |
1 { "Send LM & NTLM - use NTLMv2 session security if negotiated" } | |
0 { "Send LM & NTLM responses" } | |
Default { | |
switch ((Get-ADCOmputer ($dc).split(".")[0] -Properties operatingsystem -Server $PDC).operatingsystem ) { | |
{ $_ -like "*2022*" -OR $_ -like "*2019*" -OR $_ -like "*2016*" -OR $_ -like "*2012 R2*" } { "Send NTLMv2 response only. Refuse LM & NTLM" } | |
Default { "Not configured, OS default assumed" } | |
} | |
} | |
} | |
$LMHash = switch ($results[1]) { | |
1 { "Enabled" } | |
0 { "Disabled" } | |
Default { "Not configured" } | |
} | |
$RestrictAnnon = switch ($results[2]) { | |
0 { "Disabled" } | |
1 { "Enabled" } | |
Default { "Not configured" } | |
} | |
$LDAPIntegrity = switch ($results[3]) { | |
0 { "Does not requires signing" } | |
1 { "Requires signing" } | |
Default { "Not configured" } | |
} | |
$InactivityTimeout = switch ( $results[4] ) { | |
{ $_ -le 900 -AND $_ -ne 0 -AND $_ -ne $null } { "900 or fewer second(s), but not 0: $($_)" } | |
{ $_ -eq 0 } { "0 second" } | |
{ $_ -gt 900 } { "More than 900 seconds: $($_) seconds" } | |
Default { | |
switch ((Get-ADCOmputer ($dc).split(".")[0] -Properties operatingsystem -Server $PDC).operatingsystem ) { | |
{ $_ -like "*2022*" -OR $_ -like "*2019*" -OR $_ -like "*2016*" -OR $_ -like "*2012*" } { "OS default: 900 second" } | |
Default { "Unlimited" } | |
} | |
} | |
} | |
$settings = ($NTLM, $LMHash, $RestrictAnnon, $LDAPIntegrity, $InactivityTimeout) | |
} | |
else { | |
$settings = ("Access denied", "Access denied", "Access denied", "Access denied", "Access denied") | |
Write-Log -logtext "Could not check for security related security settings on domain controller $dc as regisitry not accessible : $($_.exception.message)" -logpath $logpath | |
} | |
if (Test-WSMan -ComputerName $DC -ErrorAction SilentlyContinue) { | |
try { | |
$settings += invoke-command -ComputerName $DC -Credential $Credential -ScriptBlock { | |
$null = secedit.exe /export /areas USER_RIGHTS /cfg "$env:TEMP\secedit.cfg" | |
$seceditContent = Get-Content "$env:TEMP\secedit.cfg" | |
$LocalLogonSIDs = ((($seceditContent | Select-String "SeInteractiveLogonRight") -split "=")[1] -replace "\*", "" -replace " ", "") -split "," | |
$LocalLogonUsers = $LocalLogonSIDs | Where-Object { $_ -ne "" } | ForEach-Object { | |
$SID = New-Object System.Security.Principal.SecurityIdentifier($_) | |
$User = $SID.Translate([System.Security.Principal.NTAccount]) | |
$User.Value | |
} | |
$LocalLogonUsers -join "`n" | |
$RemoteLogonSIDs = ((($seceditContent | Select-String "SeRemoteInteractiveLogonRight") -split "=")[1] -replace "\*", "" -replace " ", "") -split "," | |
$RemoteLogonUsers = $RemoteLogonSIDs | Where-Object { $_ -ne "" } | ForEach-Object { | |
$SID = New-Object System.Security.Principal.SecurityIdentifier($_) | |
$User = $SID.Translate([System.Security.Principal.NTAccount]) | |
$User.Value | |
} | |
$RemoteLogonUsers -join "`n" | |
$DenyNetworkLogonSIDs = ((($seceditContent | Select-String "SeDenyNetworkLogonRight") -split "=")[1] -replace "\*", "" -replace " ", "") -split "," | |
$DenyNetworkLogonUsers = $DenyNetworkLogonSIDs | Where-Object { $_ -ne "" } | ForEach-Object { | |
$SID = New-Object System.Security.Principal.SecurityIdentifier($_) | |
$User = $SID.Translate([System.Security.Principal.NTAccount]) | |
$User.Value | |
} | |
$DenyNetworkLogonUsers -join "`n" | |
$DenyServiceLogonSIDs = ((($seceditContent | Select-String "SeDenyServiceLogonRight") -split "=")[1] -replace "\*", "" -replace " ", "") -split "," | |
$DenyServiceLogonUsers = $DenyServiceLogonSIDs | Where-Object { $_ -ne "" } | ForEach-Object { | |
$SID = New-Object System.Security.Principal.SecurityIdentifier($_) | |
$User = $SID.Translate([System.Security.Principal.NTAccount]) | |
$User.Value | |
} | |
$DenyServiceLogonUsers -join "`n" | |
$DenyBatchLogonSIDs = ((($seceditContent | Select-String "SeDenyBatchLogonRight") -split "=")[1] -replace "\*", "" -replace " ", "") -split "," | |
$DenyBatchLogonUsers = $DenyBatchLogonSIDs | Where-Object { $_ -ne "" } | ForEach-Object { | |
$SID = New-Object System.Security.Principal.SecurityIdentifier($_) | |
$User = $SID.Translate([System.Security.Principal.NTAccount]) | |
$User.Value | |
} | |
$DenyBatchLogonUsers -join "`n" | |
$null = Remove-Item "$env:TEMP\secedit.cfg" | |
} | |
} | |
catch { | |
$settings += ("Access denied", "Access denied", "Access denied", "Access denied", "Access denied") | |
Write-Log -logtext "Could not check for secedit related security settings on domain controller $dc : $($_.Exception.Message)" -logpath $logpath | |
} | |
} | |
else { | |
$settings += ("Access denied", "Access denied", "Access denied", "Access denied", "Access denied") | |
Write-Log -logtext "Could not check for security related security settings on domain controller $dc as PS remoting not available : $($_.exception.message)" -logpath $logpath | |
} | |
$SecuritySettings += [PSCustomObject]@{ | |
DomainName = $DomainName | |
DCName = $DC | |
"Network security: LAN Manager authentication level" = $settings[0] | |
"Network security: Do not store LAN Manager hash value on next password change" = $settings[1] | |
"Network access: Allow anonymous SID/Name translation" = $settings[2] | |
"Domain controller: LDAP server signing requirements" = $settings[3] | |
"Interactive logon: Machine inactivity limit" = $settings[4] | |
"Allow logon locally on domain controllers" = $settings[5] | |
"Allow logon through Terminal Services on domain controllers" = $settings[6] | |
"Deny access to this computer from the network" = $settings[7] | |
"Deny log on as a service" = $settings[8] | |
"Deny log on as a batch job" = $settings[9] | |
} | |
} | |
else { | |
$SecuritySettings += [PSCustomObject]@{ | |
DomainName = $DomainName | |
DCName = $DC | |
"Network security: LAN Manager authentication level" = "DC is down" | |
"Network security: Do not store LAN Manager hash value on next password change" = "DC is down" | |
"Network access: Allow anonymous SID/Name translation" = "DC is down" | |
"Domain controller: LDAP server signing requirements" = "DC is down" | |
"Interactive logon: Machine inactivity limit" = "DC is down" | |
"Allow logon locally on domain controllers" = "DC is down" | |
"Allow logon through Terminal Services on domain controllers" = "DC is down" | |
"Deny access to this computer from the network" = "DC is down" | |
"Deny log on as a service" = "DC is down" | |
"Deny log on as a batch job" = "DC is down" | |
} | |
Write-Log -logtext "Could not check for security related security settings on domain controller $dc as DC is down : $($_.exception.message)" -logpath $logpath | |
} | |
$message = "Working over domain: $DomainName Domain Controller $DC security checks." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
return $SecuritySettings | |
} | |
# This function identifies unused Netlogon scripts in the Active Directory domain. | |
function Get-UnusedNetlogonScripts { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, Mandatory = $false)][pscredential]$Credential | |
) | |
$unusedScripts = @() | |
$referencedScripts = @() | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$netlogonPath = "\\$DomainName\netlogon" | |
try { | |
$scriptFiles = Get-ChildItem -Path $netlogonPath -File -Recurse | Select-Object -ExpandProperty FullName | |
} | |
catch { | |
Write-Log -logtext "Could not access Netlogon share to read script files : $($_.Exception.Message)" -logpath $logpath | |
} | |
$scriptFiles = $scriptfiles -replace $DomainName, $DomainName.Split(".")[0] | Where-Object { $_ -ne $null } | Sort-Object -Unique | |
$referencedScripts = (Get-ADuser -filter * -Server $PDC -Properties ScriptPath | Where-Object { $_.ScriptPath } | Sort-Object ScriptPath -Unique).ScriptPath | |
if ($scriptFiles) { | |
$gpos = Get-GPO -All -Domain $DomainName -Server $PDC | |
foreach ($gpo in $gpos) { | |
$gpoReport = Get-GPOReport -Name $gpo.DisplayName -ReportType Xml -Domain $DomainName -Server $PDC | |
$gpoXml = [xml]$gpoReport | |
$computerScripts = $gpoXml.GPO.Computer.ExtensionData.Extension.Script | Select-Object -ExpandProperty Command | |
$userScripts = $gpoXml.GPO.User.ExtensionData.Extension.Script | Select-Object -ExpandProperty Command | |
$referencedScripts += $computerScripts, $userScripts | |
} | |
$referencedScripts = $referencedScripts -replace $DomainName, $DomainName.Split(".")[0] | Where-Object { $_ -ne $null } | Sort-Object -Unique | |
if ($null -ne $referencedScripts ) { | |
$unused = Compare-Object -ReferenceObject $scriptFiles -DifferenceObject $referencedScripts | Where-Object { $_.SideIndicator -eq '<=' } | Select-Object -ExpandProperty InputObject | |
} | |
else { | |
$unused = $scriptFiles | |
} | |
} | |
$unused = $unused | Where-Object { $_ -ne $null } | Sort-Object -Unique | |
$unusedScripts = [PSCustomObject]@{ | |
DomainName = $DomainName | |
UnusedScripts = $unused -join "`n" | |
} | |
return $unusedScripts | |
} | |
function Get-PotentialSvcAccount { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, Mandatory = $false)][pscredential]$Credential | |
) | |
} | |
# This function retrieves the permissions set on the SYSVOL and NETLOGON shares in the Active Directory domain. | |
function Get-SysvolNetlogonPermissions { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, Mandatory = $false)][pscredential]$Credential | |
) | |
$SYSVOLPermsummary = @() | |
$NETLOGONPermsummary = @() | |
$SysvolNetlogonPermissions = @() | |
try { | |
$SYSVOLACLs = Get-ACL "\\$DomainName\SYSVOL" | |
} | |
catch { | |
Write-Log -logtext "Could not get SYSVOL share permissions : $($_.Exception.Message)" -logpath $logpath | |
} | |
try { | |
$NETLOGONACLs = Get-ACL "\\$DomainName\NETLOGON" | |
} | |
catch { | |
Write-Log -logtext "Could not get NETLOGON share permissions : $($_.Exception.Message)" -logpath $logpath | |
} | |
ForEach ($SYSVOLACL in $SYSVOLACLs.Access) { | |
Switch ([long]$SYSVOLACL.FileSystemRights) { | |
2032127 { $AccessMask = "FullControl" } | |
1179785 { $AccessMask = "Read" } | |
1180063 { $AccessMask = "Read, Write" } | |
1179817 { $AccessMask = "ReadAndExecute" } | |
{ -1610612736 } { $AccessMask = "ReadAndExecuteExtended" } | |
1245631 { $AccessMask = "ReadAndExecute, Modify, Write" } | |
1180095 { $AccessMask = "ReadAndExecute, Write" } | |
268435456 { $AccessMask = "FullControl (Sub Only)" } | |
{ $_ -notmatch '^[0-9]+$' -AND -NOT($_ -in ("-536084480")) } { $AccessMask = $SYSVOLACL.FileSystemRights } | |
default { $AccessMask = "SpecialPermissions" } | |
} | |
$IdentityReference = $SYSVOLACL.Identityreference.Value | |
$AccessType = $AccessMask | |
$AccessControlType = $SYSVOLACL.AccessControlType | |
$SYSVOLPermsummary += ("$IdentityReference, $AccessType, $AccessControlType") | |
} | |
ForEach ($NETLOGONACL in $NETLOGONACLs.Access) { | |
Switch ([long]$NETLOGONACL.FileSystemRights) { | |
2032127 { $AccessMask = "FullControl" } | |
1179785 { $AccessMask = "Read" } | |
1180063 { $AccessMask = "Read, Write" } | |
1179817 { $AccessMask = "ReadAndExecute" } | |
{ -1610612736 } { $AccessMask = "ReadAndExecuteExtended" } | |
1245631 { $AccessMask = "ReadAndExecute, Modify, Write" } | |
1180095 { $AccessMask = "ReadAndExecute, Write" } | |
268435456 { $AccessMask = "FullControl (Sub Only)" } | |
{ $_ -notmatch '^[0-9]+$' -AND -NOT($_ -in ("-536084480")) } { $AccessMask = $SYSVOLACL.FileSystemRights } | |
default { $AccessMask = "SpecialPermissions" } | |
} | |
$IdentityReference = $NETLOGONACL.Identityreference.Value | |
$AccessType = $AccessMask | |
$AccessControlType = $NETLOGONACL.AccessControlType | |
$NETLOGONPermsummary += ("$IdentityReference, $AccessType, $AccessControlType") | |
} | |
$SysvolNetlogonPermissions += [pscustomobject] @{ | |
DomainName = $DomainName | |
ShareName = "SYSVOL" | |
Sharepermissions = $SYSVOLPermsummary -join "`n" | |
IsInherited = -NOT($SYSVOLACLs.AreAccessRulesProtected) | |
} | |
$SysvolNetlogonPermissions += [pscustomobject] @{ | |
DomainName = $DomainName | |
ShareName = "NETLOGON" | |
Sharepermissions = $NETLOGONPermsummary -join "`n" | |
IsInherited = -NOT($NETLOGONACLs.AreAccessRulesProtected) | |
} | |
return $SysvolNetlogonPermissions | |
} | |
# This function collects detailed system information from client computers in the Active Directory domain, including hardware, software, and network configuration. | |
Function Get-SystemInfo { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, Mandatory = $false)][pscredential]$Credential, | |
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]$Servers | |
) | |
$PDC = (Test-Connection -Computername (Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential).Hostname -count 2 -AsJob | Get-Job | Receive-Job -Wait | sort-object Responsetime | select-Object Address -first 1).Address | |
$servers = $servers | ForEach-Object { Get-ADComputer -Identity $_ -Server $PDC -properties Name, IPv4Address, OperatingSystem } | select-object Name, IPv4Address, OperatingSystem | |
#Run the commands for each server in the list | |
$infoObject = @() | |
Foreach ($s in $servers) { | |
$ErrorActionPreference = "Continue" | |
try { | |
$CPUInfo = Get-CimInstance Win32_Processor -ComputerName $s.Name | |
$PhysicalMemory = Get-CimInstance CIM_PhysicalMemory -ComputerName $s.Name | Measure-Object -Property capacity -Sum | ForEach-Object { [Math]::Round(($_.sum / 1GB), 2) } | |
$NetworkInfo = Get-CimInstance Win32_networkadapter -ComputerName $s.Name | Where-Object { $_.MACAddress -AND $_.PhysicalAdapter -eq $true } | |
$DiskInfo = Get-CimInstance Win32_LogicalDisk -ComputerName $s.Name | |
$SerialNumber = (Get-CimInstance Win32_BIOs -ComputerName $s.Name).SerialNumber | |
$MakeInfo = Get-CimInstance Win32_ComputerSystem -ComputerName $s.Name | |
$NICSpeed = (($NetworkInfo.Speed | ForEach-Object { ([Math]::Round(($_ / 1GB), 2)) }) -Join " Gbps,") + " Gbps" | |
$DiskSizes = (($DiskInfo.size | ForEach-Object { ([Math]::Round(($_ / 1GB), 2)) }) -join " GB,") + " GB" | |
$DiskFreeSizes = (($DiskInfo.FreeSpace | ForEach-Object { ([Math]::Round(($_ / 1GB), 2)) }) -join " GB,") + " GB" | |
$infoObject += [PSCustomObject]@{ | |
Name = $s.Name | |
IPAddress = $s.IPV4Address | |
SerialNumber = $SerialNumber | |
Manufacturer = $MakeInfo.Manufacturer | |
Model = $MakeInfo.Model | |
OperatingSystem = $s.OperatingSystem | |
Processor = ($CPUInfo.Name -join ",") | |
PhysicalCores = ($CPUInfo.NumberOfCores -join ",") | |
Logicalcores = ($CPUInfo.NumberOfLogicalProcessors -join ",") | |
PhysicalMemory = $PhysicalMemory | |
NIC_Count = ($NetworkInfo | Measure-object).Count | |
NIC_Name = ($NetworkInfo.NetConnectionID -join ",") | |
NIC_MAC = ($NetworkInfo.MACAddress -join ",") | |
NIC_Speed = $NICSpeed | |
DriveLetter = ($DiskInfo.DeviceID -join ",") | |
DriveSize = $DiskSizes | |
DriveFreeSpace = $DiskFreeSizes | |
} | |
} | |
catch { | |
Write-Log -logtext "Could not get inventory info from server $($s.Name) : $($_.Exception.Message)" -logpath $logpath | |
Continue | |
} | |
$message = "Working over domain: $DomainName Domain Controller $($s.Name) inventory details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
Return $infoObject | |
} | |
# This function generates an email message. | |
function New-Email { | |
[CmdletBinding()] | |
param( | |
[parameter(mandatory = $true)]$RecipientAddressTo, | |
[parameter(mandatory = $true)]$SenderAddress, | |
[parameter(mandatory = $true)]$SMTPServer, | |
[parameter(mandatory = $true)]$Subject, | |
[parameter(mandatory = $true)]$Body, | |
[parameter(mandatory = $false)]$SMTPServerPort = "25", | |
[parameter(mandatory = $false)]$RecipientAddressCc, | |
[parameter(mandatory = $false)][pscredential]$credential | |
) | |
if ($RecipientAddressCc) { | |
try { | |
$email = @{ | |
From = $SenderAddress | |
To = $RecipientAddressTo | |
Cc = $RecipientAddressCc | |
Subject = $Subject | |
Body = $Body | |
SmtpServer = $SMTPServer | |
Port = $SMTPServerPort | |
} | |
Send-MailMessage @email -UseSsl -BodyAsHtml -Credential $credential | |
} | |
Catch { | |
Throw $_.exception.message | |
} | |
} | |
else { | |
try { | |
$email = @{ | |
From = $SenderAddress | |
To = $RecipientAddressTo | |
Subject = $Subject | |
Body = $Body | |
SmtpServer = $SMTPServer | |
Port = $SMTPServerPort | |
} | |
Send-MailMessage @email -UseSsl -BodyAsHtml -Credential $credential | |
} | |
Catch { | |
Throw $_.exception.message | |
} | |
} | |
} | |
# This function creates a balloon notification to display on client computers. | |
function New-BaloonNotification { | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)][String]$title, | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)][String]$message, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][ValidateSet('None', 'Info', 'Warning', 'Error')][String]$icon = "Info", | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][scriptblock]$Script | |
) | |
Add-Type -AssemblyName System.Windows.Forms | |
if ($null -eq $script:balloonToolTip) { $script:balloonToolTip = New-Object System.Windows.Forms.NotifyIcon } | |
$tip = New-Object System.Windows.Forms.NotifyIcon | |
$path = Get-Process -id $pid | Select-Object -ExpandProperty Path | |
$tip.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path) | |
$tip.BalloonTipIcon = $Icon | |
$tip.BalloonTipText = $message | |
$tip.BalloonTipTitle = $title | |
$tip.Visible = $true | |
try { | |
register-objectevent $tip BalloonTipClicked BalloonClicked_event -Action { $script.Invoke() } | Out-Null | |
} | |
catch {} | |
$tip.ShowBalloonTip(15000) # Even if we set it for 1000 milliseconds, it usually follows OS minimum 10 seconds | |
Start-Sleep -s 1 | |
$tip.Dispose() # Important to dispose otherwise the icon stays in notifications till reboot | |
Get-EventSubscriber -SourceIdentifier "BalloonClicked_event" -ErrorAction SilentlyContinue | Unregister-Event # In case if the Event Subscription is not disposed | |
} | |
# This function performs a health check of the Active Directory environment, including checks for replication, DNS, AD trust, and other common issues. | |
function Test-ADHealth { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$Report = @() | |
$dcs = Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential | |
#foreach domain controller | |
foreach ($Dcserver in $dcs.hostname) { | |
if (Test-Connection -ComputerName $Dcserver -Count 4 -Quiet) { | |
try { | |
$setping = "OK" | |
# Netlogon Service Status | |
$DcNetlogon = Get-Service -ComputerName $Dcserver -Name "Netlogon" -ErrorAction SilentlyContinue | |
if ($DcNetlogon.Status -eq "Running") { | |
$setnetlogon = "ok" | |
} | |
else { | |
$setnetlogon = "$DcNetlogon.status" | |
} | |
#NTDS Service Status | |
$dcntds = Get-Service -ComputerName $Dcserver -Name "NTDS" -ErrorAction SilentlyContinue | |
if ($dcntds.Status -eq "running") { | |
$setntds = "ok" | |
} | |
else { | |
$setntds = "$dcntds.status" | |
} | |
#DNS Service Status | |
$dcdns = Get-Service -ComputerName $Dcserver -Name "DNS" -ea SilentlyContinue | |
if ($dcdns.Status -eq "running") { | |
$setdcdns = "ok" | |
} | |
else { | |
$setdcdns = "$dcdns.Status" | |
} | |
#Dcdiag netlogons "Checking now" | |
$dcdiagnetlogon = dcdiag /test:netlogons /s:$dcserver | |
if ($dcdiagnetlogon -match "passed test NetLogons") { | |
$setdcdiagnetlogon = "ok" | |
} | |
else { | |
$setdcdiagnetlogon = (($dcdiagnetlogon | select-string "Error", "warning" | ForEach-Object { $_.line.trim() }) -join "`n") + "`n`nRun dcdiag /test:netlogons /s:$dcserver" | |
} | |
#Dcdiag services check | |
$dcdiagservices = dcdiag /test:services /s:$dcserver | |
if ($dcdiagservices -match "passed test services") { | |
$setdcdiagservices = "ok" | |
} | |
else { | |
$setdcdiagservices = (($dcdiagservices | select-string "Error", "warning" | ForEach-Object { $_.line.trim() }) -join "`n") + "`n`nRun dcdiag /test:services /s:$dcserver" | |
} | |
#Dcdiag Replication Check | |
$dcdiagreplications = dcdiag /test:Replications /s:$dcserver | |
if ($dcdiagreplications -match "passed test Replications") { | |
$setdcdiagreplications = "ok" | |
} | |
else { | |
$setdcdiagreplications = (($dcdiagreplications | select-string "Error", "warning" | ForEach-Object { $_.line.trim() }) -join "`n") + "`n`nRun dcdiag /test:Replications /s:$dcserver" | |
} | |
#Dcdiag FSMOCheck Check | |
$dcdiagFsmoCheck = dcdiag /test:FSMOCheck /s:$dcserver | |
if ($dcdiagFsmoCheck -match "passed test FsmoCheck") { | |
$setdcdiagFsmoCheck = "ok" | |
} | |
else { | |
$setdcdiagFsmoCheck = (($dcdiagFsmoCheck | select-string "Error", "warning" | ForEach-Object { $_.line.trim() }) -join "`n") + "`n`nRun dcdiag /test:FSMOCheck /s:$dcserver" | |
} | |
#Dcdiag Advertising Check | |
$dcdiagAdvertising = dcdiag /test:Advertising /s:$dcserver | |
if ($dcdiagAdvertising -match "passed test Advertising") { | |
$setdcdiagAdvertising = "ok" | |
} | |
else { | |
$setdcdiagAdvertising = (($dcdiagAdvertising | select-string "Error", "warning" | ForEach-Object { $_.line.trim() }) -join "`n") + "`n`nRun dcdiag /test:Advertising /s:$dcserver" | |
} | |
$tryok = "ok" | |
} | |
catch { | |
Write-Log -logtext "Could not check $dcserver for health : $($_.exception.message)" -logpath $logpath | |
} | |
if ($tryok -eq "ok") { | |
$Report += [PSCustomObject]@{ | |
DCName = $Dcserver | |
Ping = $setping | |
Netlogon = $setnetlogon | |
NTDS = $setntds | |
DNS = $setdcdns | |
DCDIAG_Netlogons = $setdcdiagnetlogon | |
DCDIAG_Services = $setdcdiagservices | |
DCDIAG_Replications = $setdcdiagreplications | |
DCDIAG_FSMOCheck = $setdcdiagFsmoCheck | |
DCDIAG_Advertising = $setdcdiagAdvertising | |
} | |
#set DC status | |
$setdcstatus = "ok" | |
} | |
} | |
else { | |
$setdcstatus = "DC is down" | |
$Report += [PSCustomObject]@{ | |
DCName = $Dcserver | |
Ping = $setdcstatus | |
Netlogon = $setdcstatus | |
NTDS = $setdcstatus | |
DNS = $setdcstatus | |
DCDIAG_Netlogons = $setdcstatus | |
DCDIAG_Services = $setdcstatus | |
DCDIAG_Replications = $setdcstatus | |
DCDIAG_FSMOCheck = $setdcstatus | |
DCDIAG_Advertising = $setdcstatus | |
} | |
} | |
$message = "Working over domain: $DomainName Domain Controller $DCserver health checks." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
Return $Report | |
} | |
# This function checks the replication health of domain controllers in the Active Directory domain. | |
function Get-ADReplicationHealth { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)]$DomainName, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][pscredential]$Credential | |
) | |
$replicationData = @() | |
$domainControllers = Get-ADDomainController -Filter * -Server $DomainName -Credential $Credential | |
foreach ($dc in $domainControllers) { | |
try { | |
$dcName = $dc.Name | |
$replicationInfo = Get-ADReplicationPartnerMetadata -Target $dcName -Credential $Credential -ErrorAction SilentlyContinue | |
$replicationFailures = Get-ADReplicationFailure -Target $dcName -Credential $Credential -ErrorAction SilentlyContinue | |
foreach ($partner in $replicationInfo) { | |
$partnerData = Get-ADDomainController -Identity $partner.Partner -Server $DomainName -Credential $Credential | |
$replicationStatus = $partner.LastReplicationResult | |
$lastReplicationTime = $partner.LastReplicationSuccess | |
$LastReplicationAttempt = $partner.LastReplicationAttempt | |
$failure = $replicationFailures | Where-Object { $_.Partner -eq $partner.Partner } | |
$replicationData += [PSCustomObject] @{ | |
DomainController = $dcName | |
Partner = $partnerData.Name | |
ReplicationStatus = $replicationStatus | |
LastReplicationSuccessTime = $lastReplicationTime | |
LastReplicationTimeAttempt = $LastReplicationAttempt | |
FirstFailureTime = $failure.FirstFailureTime -join "`n" | |
FailureCount = $failure.FailureCount -join "`n" | |
FailureType = $failure.FailureType -join "`n" | |
FailureError = $failure.LastError -join "`n" | |
} | |
} | |
} | |
catch { | |
Write-Log -logtext "Could not check $($dc.Name) for replication health : $($_.exception.message)" -logpath $logpath | |
} | |
} | |
return $replicationData | |
} | |
# This function retrieves detailed information about the Active Directory forest and generates the html report. | |
Function Get-ADForestDetails { | |
[CmdletBinding()] | |
Param( | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)]$Logo = $logopath, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)]$ReportPath = $ReportPath1, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)]$CSSHeader = $header, | |
[Parameter(ValueFromPipeline = $true, mandatory = $true)][pscredential]$Credential, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)]$ChildDomain, | |
[Parameter(ValueFromPipeline = $true, mandatory = $false)][switch]$ADFS | |
) | |
# Collecting information about current Forest configuration | |
$ForestInfo = Get-ADForest -Current LocalComputer -Credential $Credential | |
$forest = $ForestInfo.RootDomain | |
$message = "Collecting data about forest: $forest ." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$allDomains = $ForestInfo.Domains | |
$ForestGC = $ForestInfo.GlobalCatalogs | |
$forestFL = $ForestInfo.ForestMode | |
$FSMODomainNaming = $ForestInfo.DomainNamingMaster | |
$FSMOSchema = $ForestInfo.SchemaMaster | |
$forestDomainSID = (Get-ADDomain $forest -Server $forest -Credential $Credential).domainSID.Value | |
$SchemaPartition = $ForestInfo.PartitionsContainer.Replace("CN=Partitions", "CN=Schema") | |
$SchemaVersion = Get-ADObject -Server $forest -Identity $SchemaPartition -Credential $Credential -Properties * | Select-Object objectVersion | |
$configPartition = $ForestInfo.PartitionsContainer.Replace("CN=Partitions,", "") | |
# Check if AD Recycle Bin support is enabled | |
if ( $forestFL -in ("Windows2008R2Forest", "Windows2012Forest", "Windows2012R2Forest", "Windows2016Forest")) { | |
$ADR = (Get-ADOptionalFeature 'Recycle Bin Feature' -Server $forest -Credential $Credential).EnabledScopes.count | |
if ( $ADR.Count -ne 0 ) { | |
$ADRSupport = "Enabled" | |
} | |
else { | |
$ADRSupport = "Disabled" | |
} | |
} | |
switch ($SchemaVersion.objectVersion) { | |
13 { $ForestSchemaVersion = "Windows 2000 Server" } | |
30 { $ForestSchemaVersion = "Windows Server 2003" } | |
31 { $ForestSchemaVersion = "Windows Server 2003 R2" } | |
44 { $ForestSchemaVersion = "Windows Server 2008" } | |
47 { $ForestSchemaVersion = "Windows Server 2008 R2" } | |
51 { $ForestSchemaVersion = "Windows Server 8 Developers Preview" } | |
52 { $ForestSchemaVersion = "Windows Server 8 Beta" } | |
56 { $ForestSchemaVersion = "Windows Server 2012" } | |
69 { $ForestSchemaVersion = "Windows Server 2012 R2" } | |
87 { $ForestSchemaVersion = "Windows Server 2016" } | |
88 { $ForestSchemaVersion = "Windows Server 2019/2022" } | |
default { $ForestSchemaVersion = "unknown forest schema version: ($SchemaVersion.objectVersion)" } | |
} | |
switch ($forestFL) { | |
Windows2000Forest { $ForestFunctionalLevel = "Windows 2000" } | |
Windows2003Forest { $ForestFunctionalLevel = "Windows Server 2003" } | |
Windows2008Forest { $ForestFunctionalLevel = "Windows Server 2008" } | |
Windows2008R2Forest { $ForestFunctionalLevel = "Windows Server 2008 R2" } | |
Windows2012Forest { $ForestFunctionalLevel = "Windows Server 2012" } | |
Windows2012R2Forest { $ForestFunctionalLevel = "Windows Server 2012 R2" } | |
Windows2016Forest { $ForestFunctionalLevel = "Windows Server 2016" } | |
default { $ForestFunctionalLevel = "Unknown Forest Functional Level: $forestFL" } | |
} | |
$tombstoneLifetime = (Get-ADobject -Server $forest -Identity "cn=Directory Service,cn=Windows NT,cn=Services,$configPartition" -Credential $Credential -Properties tombstoneLifetime).tombstoneLifetime | |
if ($null -eq $tombstoneLifetime) { | |
$tombstoneLifetime = 60 | |
} | |
$ForestDetails = [PSCustomObject]@{ | |
ForestName = $forest | |
ForestFunctionalLevel = $ForestFunctionalLevel | |
ForestSchemaVersion = $ForestSchemaVersion | |
DomainNamingMaster = $FSMODomainNaming | |
SchemaMaster = $FSMOSchema | |
GlobalCatalogs = $ForestGC.count | |
DomainCount = $allDomains.count | |
RecycleBinSupport = $ADRSupport | |
TombstoneLifeTime = $tombstoneLifetime | |
} | |
$ForestSummary = ($ForestDetails | ConvertTo-Html -As List -Property ForestName, ForestFunctionLevel, ForestSchemaVersion, DomainNamingMaster, SchemaMaster, GlobalCatalogs, DomainCount, RecycleBinSupport, TombstoneLifetime -Fragment -PreContent "<h2>Forest Summary: $forest</h2>") | |
$entGroupID = $forestDomainSID + "-519" | |
$enterpriseAdminsNo = @(Get-ADGroup -Server $forest -Identity $entGroupID -Credential $Credential | Get-ADGroupMember -Recursive).count | |
$schemaGroupID = $forestDomainSID + "-518" | |
$schmaAdminsNo = @(Get-ADGroup -Server $forest -Identity $schemaGroupID -Credential $Credential | Get-ADGroupMember -Recursive).count | |
$ForestPrivGroups = [PSCustomObject]@{ | |
ForestName = $forest | |
EnterpriseAdminGroup = (Get-ADGroup -Server $forest -Identity $entGroupID -Credential $Credential).Name | |
EntAdminMemberCount = $enterpriseAdminsNo | |
SchemaAdminGroup = (Get-ADGroup -Server $forest -Identity $schemaGroupID -Credential $Credential).Name | |
SchemaAdminMemberCount = $schmaAdminsNo | |
} | |
$ForestPrivGroupsSummary = ($ForestPrivGroups | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Forest-wide Priviledged groups Summary</h2>") -replace "`n", "<br>" | |
# PKI details to be picked from schema only if the script is being run from the forest level | |
if ($forestcheck) { | |
$PKIDetails = Get-PKIDetails -ForestName $forest -Credential $Credential | |
} | |
# Setting up variables, which would collect the information from all the domains | |
$TrustDetails = @() | |
$DomainDetails = @() | |
$SiteDetails = @() | |
$privGroupDetails = @() | |
$UserDetails = @() | |
$BuiltInUserDetails = @() | |
$GroupDetails = @() | |
$UndesiredAdminCount = @() | |
$PasswordPolicyDetails = @() | |
$FGPwdPolicyDetails = @() | |
$ObjectsToClean = @() | |
$OrphanedFSPDetails = @() | |
$ServerOSDetails = @() | |
$ClientOSDetails = @() | |
$ADFSDetails = @() | |
$ADSyncDetails = @() | |
$DNSServerDetails = @() | |
$DNSZoneDetails = @() | |
$EmptyOUDetails = @() | |
$GPOSummaryDetails = @() | |
$GPODetails = @() | |
$SecuritySettings = @() | |
$unusedScripts = @() | |
$SysvolNetlogonPermissions = @() | |
$DCInventory = @() | |
$ADHealth = @() | |
$ReplicationHealth = @() | |
if (!($forestcheck)) { | |
$allDomains = $ChildDomain | |
} | |
$message = "Summary details about forest: $forest done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
# This section collects information from all domains | |
ForEach ($domain in $allDomains) { | |
$message = "Working over domain: $Domain related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$TrustDetails += Get-ADTrustDetails -DomainName $domain -credential $Credential | |
$DomainDetails += Get-ADDomainDetails -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain Health checks" | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$ADHealth += Test-ADHealth -DomainName $domain -Credential $Credential | |
$ReplicationHealth += Get-ADReplicationHealth -DomainName $domain -Credential $Credential | |
$message = "The domain: $Domain Health checks done" | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$message = "Working over domain: $Domain DC inventory related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$DCInventory += Get-SystemInfo -DomainName $domain -Credential $Credential -server ($DomainDetails | Where-Object { $_.Domain -eq $domain }).DCName | |
$message = "The domain: $Domain DC inventory related details collected." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$SiteDetails += Get-ADSiteDetails -DomainName $domain -credential $Credential | |
$privGroupDetails += Get-PrivGroupDetails -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain user summary related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$UserDetails += Get-ADUserDetails -DomainName $domain -credential $Credential | |
$BuiltInUserDetails += Get-BuiltInUserDetails -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain Group summary related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$GroupDetails += Get-ADGroupDetails -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain AdminCount enabled user related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$UndesiredAdminCount += Get-AdminCountDetails -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain password policy related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$PasswordPolicyDetails += Get-ADPasswordPolicy -DomainName $domain -credential $Credential | |
$FGPwdPolicyDetails += Get-FineGrainedPasswordPolicy -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain orpahed/lingering objects related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$ObjectsToClean += Get-ADObjectsToClean -DomainName $domain -credential $Credential | |
$OrphanedFSPDetails += Get-OrphanedFSP -DomainName $domain -credential $Credential | |
$message = "The orpahed/lingering objects related details from domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$ServerOSDetails += Get-DomainServerDetails -DomainName $domain -credential $Credential | |
$ClientOSDetails += Get-DomainClientDetails -DomainName $domain -credential $Credential | |
if ($ADFS) { | |
$message = "Looking for ADFS/ ADSync server in domain: $Domain. It might take long time" | |
New-BaloonNotification -title "Caution" -message $message -icon Warning | |
Write-Log -logtext $message -logpath $logpath | |
$ADSyncDetail, $ADFSDetail = Get-ADFSDetails -DomainName $domain -credential $Credential | |
$ADFSDetail = $ADFSDetail | Sort-Object * -Unique | |
$ADFSDetails += $ADFSDetail | |
$ADSyncDetail = $ADSyncDetail | Sort-Object * -Unique | |
$ADSyncDetails += $ADSyncDetail | |
$message = "Lookup for ADFS ($($ADFSDetail.SERVERNAME.count)) / ADSync ($($ADSyncDetail.SERVERNAME.count)) server in domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
$message = "Working over domain: $Domain DNS related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$DNSServerDetails += Get-ADDNSDetails -DomainName $domain -credential $Credential | |
$DNSZoneDetails += Get-ADDNSZoneDetails -DomainName $domain -credential $Credential | |
$message = "DNS relation detals collection from domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$message = "Looking for empty OUs in domain: $Domain ." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$EmptyOUDetails += Get-EmptyOUDetails -DomainName $domain -credential $Credential | |
$message = "Check for empty OUs in domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$GPOSummaryDetails += Get-ADGPOSummary -DomainName $domain -credential $Credential | |
$message = "Working over domain: $Domain GPO ($(($GPOSummaryDetails | Where-Object {$_.Domain -eq $domain}).AllGPOs)) related details." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$GPODetails += Get-GPOInventory -DomainName $domain | |
$message = "GPO related details from domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$SysvolNetlogonPermissions += Get-SysvolNetlogonPermissions -DomainName $domain -Credential $Credential | |
$message = "Working over domain: $Domain security setting." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$SecuritySettings += Start-SecurityCheck -DomainName $domain -Credential $Credential | |
$message = "Security setting details from domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$message = "Checking for unused scripts in NETLOGON for domain: $Domain ." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$unusedScripts += Get-UnusedNetlogonScripts -DomainName $domain -Credential $Credential | |
$message = "Check for unused scripts in NETLOGON for domain: $Domain done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$message = "Work over domain: $Domain related details done." | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
# This scetion prepares HTML report | |
If ($TrustDetails) { | |
$TrustSummary = ($TrustDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>AD Trust Summary</h2>") | |
} | |
If ($DHCPFlag) { | |
$message = "Looking for all DHCP servesr in forest: $forest and their scope details. It might take long time" | |
New-BaloonNotification -title "Caution" -message $message -icon Warning | |
Write-Log -logtext $message -logpath $logpath | |
$DHCPDetails = Get-ADDHCPDetails -Credential $Credential | |
$DHCPInventory = Get-DHCPInventory | |
$DHCPSummary = ($DHCPDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>DHCP Server Summary</h2>") -replace "`n", "<br>" | |
$DHCPInventorySummary = ($DHCPInventory.Inventory | ConvertTo-Html -As Table -Fragment -PreContent "<h2>DHCP Server Inventory</h2>") -replace "`n", "<br>" -replace '<td>Inactive</td>', '<td bgcolor="red">Inactive</td>' | |
$DHCPResInventory = ($DHCPInventory.reservation | ConvertTo-Html -As Table -Fragment -PreContent "<h2>DHCP Server Reservation Inventory</h2>") -replace "`n", "<br>" | |
$message = "DHCP Server information in forest: $forest collected" | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
} | |
#If ($PKIDetails) { | |
$PKISummary = ($PKIDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Certificate servers Summary</h2>") -replace '<td>SHA1RSA</td>', '<td bgcolor="red">SHA1RSA</td>' | |
#} | |
If ($ADSyncDetails) { | |
$ADSyncSummary = ($ADSyncDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>ADSync servers Summary</h2>") -replace '<td>Access denied</td>', '<td bgcolor="red">Access denied</td>' | |
} | |
If ($ADFSDetails) { | |
$ADFSSummary = ($ADFSDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>ADFS servers Summary</h2>") -replace '<td>Access denied</td>', '<td bgcolor="red">Access denied</td>' -replace "`n", "<br>" | |
} | |
If ($ClientOSDetails) { | |
$ClientOSSummary = $ClientOSDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Client OS Summary</h2>" | |
} | |
If ($FGPwdPolicyDetails) { | |
$FGPwdPolicySummary = ($FGPwdPolicyDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Fine Grained Password Policy Summary</h2>") -replace "`n", "<br>" | |
} | |
If ($UndesiredAdminCount) { | |
$UndesiredAdminCountSummary = ($UndesiredAdminCount | ConvertTo-Html -As Table -Fragment -PreContent "<h2> Undesired AdminCount atribute user Summary</h2>") -replace "`n", "<br>" | |
} | |
If ($ObjectsToClean) { | |
$ObjectsToCleanSummary = ($ObjectsToClean | ConvertTo-Html -As Table -Fragment -PreContent "<h2> Orphaned and Lingering objects Summary</h2>") -replace "`n", "<br>" | |
} | |
If ($OrphanedFSPDetails) { | |
$OrphanedFSPSummary = ($OrphanedFSPDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2> Orphaned foreign security principals Summary</h2>") -replace "`n", "<br>" | |
} | |
If ($unusedScripts) { | |
$unusedScriptsSummary = ($unusedScripts | ConvertTo-Html -As Table -Fragment -PreContent "<h2> Unused Netlogon Scripts summary </h2>") -replace "`n", "<br>" | |
} | |
$DomainSummary = ($DomainDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Domains Summary</h2>") -replace '<td>Reg not found</td>', '<td bgcolor="red">Reg not found</td>' -replace "`n", "<br>" | |
$DomainHealthSumamry = ($ADHealth | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Domain Controller health Summary</h2>") -replace "`n", "<br>" -replace '<td>DC is Down</td>', '<td bgcolor="red">DC is Down</td>' | |
$ReplhealthSummary = ($ReplicationHealth | ConvertTo-Html -As Table -Fragment -PreContent "<h2>AD Replication health Summary</h2>") -replace "`n", "<br>" | |
$DNSSummary = ($DNSServerDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>DNS Servers Summary</h2>") -replace "`n", "<br>" | |
$DNSZoneSummary = ($DNSZoneDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>DNS Zones Summary</h2>") -replace "`n", "<br>" | |
$SitesSummary = ($SiteDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>AD Sites Summary</h2>" ) -replace "`n", "<br>" -replace '<td>No DC in Site</td>', '<td bgcolor="red">No DC in Site</td>' | |
$BuiltInUserSummary = $BuiltInUserDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>BuiltInUsers Summary</h2>" | |
$UserSummary = $UserDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Users Summary</h2>" | |
$GroupSummary = ($GroupDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Groups Summary</h2>") -replace "`n", "<br>" | |
$PrivGroupSummary = ($privGroupDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Priviledged groups Summary</h2>") -replace '<td>True</td>', '<td bgcolor="red">True</td>' | |
$PwdPolicySummary = $PasswordPolicyDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Password Policy Summary</h2>" | |
$ServerOSSummary = $ServerOSDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Server OS Summary</h2>" | |
$EmptyOUSummary = ($EmptyOUDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Empty OU Summary</h2>") -replace "`n", "<br>" | |
$GPOSummary = ($GPOSummaryDetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>GPO Summary</h2>") -replace "`n", "<br>" | |
$GPOInventory = ($GPODetails | ConvertTo-Html -As Table -Fragment -PreContent "<h2>GPO Inventory</h2>") -replace "`n", "<br>" | |
$SysvolNetlogonPermSummary = ($SysvolNetlogonPermissions | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Sysvol and Netlogon Permissions Summary</h2>") -replace "`n", "<br>" | |
$SecuritySummary = ($SecuritySettings | ConvertTo-Html -As List -Fragment -PreContent "<h2>Domains Security Settings Summary</h2>") -replace "`n", "<br>" -replace '<td>Access denied</td>', '<td bgcolor="red">Access denied</td>' -replace '<td>DC is Down</td>', '<td bgcolor="red">DC is Down</td>' | |
$DCSummary = ($DCInventory | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Domain Controllers Inventory</h2>") -replace "`n", "<br>" | |
$message = "Forest $forest details collected now, preparing html report" | |
New-BaloonNotification -title "Information" -message $message | |
Write-Log -logtext $message -logpath $logpath | |
$ReportRaw = ConvertTo-HTML -Body "$ForestSummary $ForestPrivGroupsSummary $TrustSummary $PKISummary $ADSyncSummary $ADFSSummary $DHCPSummary $DomainSummary $DomainHealthSumamry $ReplhealthSummary $DNSSummary $DNSZoneSummary $SitesSummary $PrivGroupSummary $UserSummary $BuiltInUserSummary $GroupSummary $UndesiredAdminCountSummary $PwdPolicySummary $FGPwdPolicySummary $ObjectsToCleanSummary $OrphanedFSPSummary $unusedScriptsSummary $ServerOSSummary $ClientOSSummary $EmptyOUSummary $GPOSummary $GPOInventory $SysvolNetlogonPermSummary $SecuritySummary $DHCPInventorySummary $DHCPResInventory $DCSummary" -Head $header -Title "Report on AD Forest: $forest" -PostContent "<p id='CreationDate'>Creation Date: $(Get-Date) $CopyRightInfo </p>" | |
$ReportRaw | Out-File $ReportPath | |
} | |
# Menu | |
Clear-Host | |
Write-Output "Menu:" | |
"Option 1: Run script over entire forest" | |
"Option 2: Run script over single domain" | |
"Option 3: Press any other key to Quit`n" | |
$choice = Read-Host "Enter your choice: " | |
Write-Output "`nDO NOT OPEN LOG FILE DURING RUN otherwise it would not record`n`n`n" | |
switch ($choice) { | |
'1' { | |
Write-Output "Type Forest Enterprise Admin credentials:" | |
$forestcheck = $true | |
$Credential = (Get-Credential) | |
try { | |
$test = Get-ADForest -Current LocalComputer -Credential $Credential | |
} | |
catch { | |
$test = $null | |
} | |
if ($test) { | |
Get-ADForestDetails -Credential $Credential -ADFS | |
} | |
else { | |
Write-Host "Credentials not working" | |
break | |
} | |
} | |
'2' { | |
$Response = Read-host "Type the domain name or just press ENTER to select current domain :" | |
if ($Response) { | |
$DomainName = $response.trim() | |
} | |
else { | |
$DomainName = (Get-ADDomain -Current LocalComputer).DNSRoot | |
} | |
$forestcheck = $false | |
Write-Output "Type Domain Admin credentials for $DomainName :" | |
$DomainCred = (Get-Credential) | |
try { | |
$test = Get-ADDomain $DomainName -Credential $DomainCred | |
} | |
catch { | |
$test = $null | |
} | |
if ($test) { | |
Get-ADForestDetails -Credential $DomainCred -ChildDomain $DomainName -ADFS | |
} | |
else { | |
Write-Host "Credentials not working" | |
break | |
} | |
} | |
default { | |
Write-Output "Incorrect reponse, script terminated" | |
Start-Sleep 2 | |
break | |
} | |
} | |
<# $MailCredential = Get-Credential -Message "Enter the password for the email account: " -UserName "[email protected]" | |
$body = Get-Content $ReportPath1 -Raw | |
New-Email -RecipientAddressTo "[email protected]" -SenderAddress "[email protected]" -SMTPServer "smtp.office365.com" -SMTPServerPort 587 -Subject "AD Assessment Report $(get-date -Uformat "%Y%m%d-%H%M%S")" -Body $body -credential $MailCredential #> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment