ActiveDirectory.tests.ps1

[CmdletBinding()]
Param(
    #Optional: The path to a snapshot of Active Directory that you want to validate against the Gold config.
    [string]
    $ADSnapshotFile,

    #The path to the 'gold' known-good snapshot of Active Directory that you want to validate against.
    # Parameter help description
    [string]
    $ADGoldFile = (Get-ChildItem (Join-Path $Pwd 'GoldConfig-*.xml') | Select-Object -Last 1).fullname
)
if ($ADGoldFile) {
    Write-Verbose "ADGoldFile : $ADGoldFile"
}
Else {
    Throw '-ADGoldFile is required and was not found automatically by searching for GoldConfig-*.xml in the current path.'
}

if ($ADSnapshotFile) {
    Write-Verbose "ADSnapShotFile : $ADSnapShotFile"
}

#Try to load the Active Directory configuration files for comparison
Try {
    If ($ADSnapshotFile) {
        Write-Verbose "Loading the AD Snapshot from: $ADSnapshotFile"
        $ADSnapshot = Import-Clixml $ADSnapshotFile
    }
    Else {
        Write-Verbose 'No AD Snapshot file specified. Attempting to get config via Get-ADConfig.'
        $ADSnapshot = Get-ADConfig
    }

    Write-Verbose "Loading the AD Gold Config from: $ADGoldFile"
    $ADGoldConfig = Import-Clixml $ADGoldFile
}
Catch {
    Write-Error "Could not load the AD 'Gold' configuration and/or load/generate a current AD snapshot."
    Throw $_
}


#Begin testing
Describe 'Active Directory Forest Operational Readiness checks' -Tags 'Forest' {

    Context 'Verifying Forest Configuration' {
        it "Forest FQDN $($ADGoldConfig.ForestInformation.RootDomain)" {
            $ADGoldConfig.ForestInformation.RootDomain |
                Should be $ADSnapshot.ForestInformation.RootDomain
        }
        it "ForestMode $($ADGoldConfig.ForestInformation.ForestMode.ToString())" {
            $ADGoldConfig.ForestInformation.ForestMode.ToString() |
                Should be $ADSnapshot.ForestInformation.ForestMode.ToString()
        }
    }

    Context 'Verifying GlobalCatalogs' {
        $ADGoldConfig.ForestInformation.GlobalCatalogs |
            ForEach-Object {
            it "Server $($_) is a GlobalCatalog" {
                $ADSnapshot.ForestInformation.GlobalCatalogs.Contains($_) |
                    Should be $true
            }
        }
    }
}

Describe 'Active Directory Domain Operational Readiness checks' -Tags 'Domain' {
    Context 'Verifying Domain Configuration' {
        it "Total Domain Controllers $($ADGoldConfig.DomainControllers.Count)" {
            $ADGoldConfig.DomainControllers.Count |
                Should be $ADSnapshot.DomainControllers.Count
        }

        $ADGoldConfig.DomainControllers.Name |
            ForEach-Object {
            it "DomainController $($_) exists" {
                $ADSnapshot.DomainControllers.Name.Contains($_) |
                    Should be $true
            }
        }
        it "DNSRoot $($ADGoldConfig.DomainInformation.DNSRoot)" {
            $ADGoldConfig.DomainInformation.DNSRoot |
                Should be $ADSnapshot.DomainInformation.DNSRoot
        }
        it "NetBIOSName $($ADGoldConfig.DomainInformation.NetBIOSName)" {
            $ADGoldConfig.DomainInformation.NetBIOSName |
                Should be $ADSnapshot.DomainInformation.NetBIOSName
        }
        it "DomainMode $($ADGoldConfig.DomainInformation.DomainMode.ToString())" {
            $ADGoldConfig.DomainInformation.DomainMode.ToString() |
                Should be $ADSnapshot.DomainInformation.DomainMode.ToString()
        }
        it "DistinguishedName $($ADGoldConfig.DomainInformation.DistinguishedName)" {
            $ADGoldConfig.DomainInformation.DistinguishedName |
                Should be $ADSnapshot.DomainInformation.DistinguishedName
        }
        it "Server $($ADGoldConfig.DomainInformation.RIDMaster) is RIDMaster" {
            $ADGoldConfig.DomainInformation.RIDMaster |
                Should be $ADSnapshot.DomainInformation.RIDMaster
        }
        it "Server $($ADGoldConfig.DomainInformation.PDCEmulator) is PDCEmulator" {
            $ADGoldConfig.DomainInformation.PDCEmulator |
                Should be $ADSnapshot.DomainInformation.PDCEmulator
        }
        it "Server $($ADGoldConfig.DomainInformation.InfrastructureMaster) is InfrastructureMaster" {
            $ADGoldConfig.DomainInformation.InfrastructureMaster |
                Should be $ADSnapshot.DomainInformation.InfrastructureMaster
        }
    }
}

Describe 'Active Directory Default Password Policy Operational Readiness checks' -Tags 'Password' {
    Context 'Verifying Default Password Policy' {
        it 'ComplexityEnabled' {
            $ADGoldConfig.DefaultPassWordPoLicy.ComplexityEnabled |
                Should be $ADSnapshot.DefaultPassWordPoLicy.ComplexityEnabled
        }
        it 'Password History count' {
            $ADGoldConfig.DefaultPassWordPoLicy.PasswordHistoryCount |
                Should be $ADSnapshot.DefaultPassWordPoLicy.PasswordHistoryCount
        }
        it "Lockout Threshold equals $($ADGoldConfig.DefaultPassWordPoLicy.LockoutThreshold)" {
            $ADGoldConfig.DefaultPassWordPoLicy.LockoutThreshold |
                Should be $ADSnapshot.DefaultPassWordPoLicy.LockoutThreshold
        }
        it "Lockout duration equals $($ADGoldConfig.DefaultPassWordPoLicy.LockoutDuration)" {
            $ADGoldConfig.DefaultPassWordPoLicy.LockoutDuration |
                Should be $ADSnapshot.DefaultPassWordPoLicy.LockoutDuration.ToString()
        }
        it "Lockout observation window equals $($ADGoldConfig.DefaultPassWordPoLicy.LockoutObservationWindow)" {
            $ADGoldConfig.DefaultPassWordPoLicy.LockoutObservationWindow |
                Should be $ADSnapshot.DefaultPassWordPoLicy.LockoutObservationWindow.ToString()
        }
        it "Min password age equals $($ADGoldConfig.DefaultPassWordPoLicy.MinPasswordAge)" {
            $ADGoldConfig.DefaultPassWordPoLicy.MinPasswordAge |
                Should be $ADSnapshot.DefaultPassWordPoLicy.MinPasswordAge.ToString()
        }
        it "Max password age equals $($ADGoldConfig.DefaultPassWordPoLicy.MaxPasswordAge)" {
            $ADGoldConfig.DefaultPassWordPoLicy.MaxPasswordAge |
                Should be $ADSnapshot.DefaultPassWordPoLicy.MaxPasswordAge.ToString()
        }
    }
}

Describe 'Active Directory Sites,subnets & sublinks Operational Readiness' -Tags 'Sites', 'Subnets', 'Sitelinks' {
    Context 'Verifying Active Directory Sites' {
        $ADGoldConfig.Sites.Name |
            ForEach-Object {
            it "Site $($_)" {
                $ADSnapshot.Sites.Name.Contains($_) |
                    Should be $true
            }
        }
    }

    Context 'Verifying Active Directory Sitelinks' {
        $lookupSiteLinks = $ADSnapshot.Sitelinks | Group-Object -AsHashTable -Property Name
        $ADGoldConfig.Sitelinks |
            ForEach-Object {
            it "Sitelink $($_.Name)" {
                $_.Name |
                    Should be $($lookupSiteLinks.$($_.Name).Name)
            }
            it "Sitelink $($_.Name) costs $($_.Cost)" {
                $_.Cost |
                    Should be $lookupSiteLinks.$($_.Name).Cost
            }
            it "Sitelink $($_.Name) replication interval $($_.ReplicationFrequencyInMinutes)" {
                $_.ReplicationFrequencyInMinutes |
                    Should be $lookupSiteLinks.$($_.Name).ReplicationFrequencyInMinutes
            }
        }
    }

    Context 'Verifying Active Directory Subnets' {
        $lookupSubnets = $ADSnapshot.SubNets | Group-Object -AsHashTable -Property Name
        $ADGoldConfig.Subnets |
            ForEach-Object {
            it "Subnet $($_.Name)" {
                $_.Name |
                    Should be $lookupSubnets.$($_.Name).Name
            }
            it "Site $($_.Site)" {
                $_.Site |
                    Should be $lookupSubnets.$($_.Name).Site
            }
        }
    }
}

Describe 'Active Directory health checks' -Tags 'ADHC' {

    Context 'Checking the output of NLTest /Query' {
        $NLTest = NLTest.exe /Query

        it 'NLTest.exe /Query Result' {
            ($NLTest | out-string).contains('Success') | Should be $true
        }
    }

    Context 'Checking the output of DCDiag for issues on all DCs' {
        $DCDiag = dcdiag.exe -a

        it 'DCDiag.exe -a Result' {
            ($DCDiag | out-string).contains('failed') | Should be $false
        }
    }

    Context 'Checking the output of RepAdmin /showrepl for replication issues' {
        (Repadmin.exe /showrepl * /csv | convertfrom-csv) | Sort-Object 'Source DSA' | Where-Object {$_.'Number of Failures' -ge 0} |
            ForEach-Object {
            it "Replication from $($_.'Source DSA') to $($_.'Destination DSA') has $($_.'Number of Failures') failures" {
                $_.'Number of Failures' | Should Not BeGreaterThan 0
            }
        }
    }

    Context 'Pinging each Domain Controller' {
        $ADGoldConfig.DomainControllers.Name | Sort-Object |
            ForEach-Object {
            it "Ping result for Domain Controller $($_)" {
                Test-Connection $_ -Quiet | Should be $true
            }
        }
    }

    Context 'Testing local Active Directory TCP ports respond' {
        # AD Ports: https://technet.microsoft.com/en-us/library/dd772723(v=ws.10).aspx
        $Ports = @(53, 88, 135, 139, 389, 445, 464, 636, 3268, 3269, 9389)

        $Ports | foreach-object {
            it "Port test for TCP $_" {
                (Test-netconnection -ComputerName $env:COMPUTERNAME -Port $_).TcpTestSucceeded | Should be $true
            }
        }
    }

    Context 'Checking local Active Directory Windows services are running' {
        $Services = @('ADWS', 'BITS', 'CertPropSvc', 'CryptSvc', 'Dfs', 'DFSR', 'DNS', 'Dnscache', 'eventlog', 'gpsvc', 'kdc',
            'LanmanServer', 'LanmanWorkstation', 'Netlogon', 'NTDS', 'NtFrs', 'RpcEptMapper', 'RpcSs', 'SamSs',
            'W32Time')

        $Services | foreach-object {
            $Svc = get-service $_
            it "Service: $($Svc.DisplayName)" {
                $Svc.status | Should be 'Running'
            }
        }
    }

    Context 'checking DNS LDAP SRV records' {
        $i = 0
        foreach ($entry in ($ADGoldConfig.LDAPDNS | Sort-Object nametarget, name, type)) {
            it "LDAP result entry $i`: $($entry.Type)" {
                $entry.Name | Should be ($ADSnapshot.LDAPDNS[$i]).Name
                $entry.NameTarget | Should be ($ADSnapshot.LDAPDNS[$i]).NameTarget
                $entry.TTL | Should be ($ADSnapshot.LDAPDNS[$i]).TTL
                $entry.Port | Should be ($ADSnapshot.LDAPDNS[$i]).Port
                $entry.IPAddress | Should be ($ADSnapshot.LDAPDNS[$i]).IPAddress
            }
            $i++
        }
    }

    Context 'checking DNS Kerberos SRV records' {
        $i = 0
        foreach ($entry in ($ADGoldConfig.KerberosDNS | Sort-Object nametarget, name, type)) {
            it "LDAP result entry $i`: $($entry.Type)" {
                $entry.Name | Should be ($ADSnapshot.KerberosDNS[$i]).Name
                $entry.NameTarget | Should be ($ADSnapshot.KerberosDNS[$i]).NameTarget
                $entry.TTL | Should be ($ADSnapshot.KerberosDNS[$i]).TTL
                $entry.Port | Should be ($ADSnapshot.KerberosDNS[$i]).Port
                $entry.IPAddress | Should be ($ADSnapshot.KerberosDNS[$i]).IPAddress
            }
            $i++
        }
    }
}