An Active Directory Health Check PowerShell script v1.0

This is a PowerShell script which offers an Active Directory Health Check.
These checks are based on my personal best practices. Some of the checks may not be applicable to your environment.

The following fundamental guidelines apply to the script:

1) Must work for all domains in a forest tree.
2) Must work on PowerShell v3 and above.
3) Must work without module dependencies, except for the PowerShell core modules.
4) Must work without Administrator privileges.
5) Must work with Microsoft Word 2007 and above.

 

The following languages are supported for both Word and the operating system the script runs on:

Catalan Danish Dutch
English Finnish French
German Norwegian Portuguese
Spanish Swedish

 

The following checks are currently included in the script:

 Users  Direct member of Domain Local group
 Users  Password never expires
 Users  Password not required
 Users  Change password at next logon
 Users  Password not changed in last 12 months
 Users  Account without expiration date
 Users  Do not require Kerberos pre-authentication
 Users  Disabled
 Groups  Privileged with more than 5 members
 Groups  Privileged with no members
 Groups  With no members
 Sites  Without a description
 Sites  No server(s)
 Sites  Without a connection
 Sites  No sitelink(s)
 Sites  Without one or more subnet(s)
 Sitelinks  With one site
 Sitelinks  With more than two sites
 Sitelinks  Without a description
 Subnets in Sites  Available but not used
 Domain Controllers  No contact in last 3 months
 Member servers  Password never expires
 Member servers  Password older than 6 months
 Member servers  Account never expires
 Member servers  Account disabled
 Organisational Unit  GPO inheritance blocked

 

 

     Get your copy of the script here!

Release notes Download
Signed (.txt) Download
Unsigned (.txt) Download
Signed (.zip) Download
Unsigned (.zip) Download
All (.zip) Download

14 comments

  1. Andrew Healey says:

    Excellent work! Thanks Jeff!

  2. Tracy Ratz says:

    Jeff: the script looks great but one suggestion. I think it would make sense to separate out the account tests based on whether the account is disabled or not. I am not sure that people care that the account’s password was not changed in 6 months if the account is disabled.
    –Tracy

  3. Jeff Wouters says:

    Hi Tracy,
    Thanks, and suggestions are always welcome 😉
    In this case it was a conscious choice, for now, to not separate out those accounts. This because some of the users of the script may want to comment out check 1 because they won’t need it. So I can’t always use the output of check 1 and filter that out from the output of check 2.
    However… there is something on my to-do list for the next major release that I think will comply with your suggestion: Exclusions to a check 😉
    Jeff.

  4. Michel de Rooij says:

    Jeff, nice work – though some things I do not get. For instance:

    1) For all switches you’re doing:
    ..
    [Switch]$PDF=$false,
    ..
    If($PDF -eq $Null)
    { $PDF = $False }
    ..
    If(!(Test-Path Variable:PDF))

    { $PDF = $False }

    What’s the point (since it will take the default value when not specified)?

    2) For certain switches you’re checking if they were provided using:
    $PSBoundParameters.ContainsKey(‘Sites’)

    Why not set/use a default value, use Test-Path Variable:Sites or check if it’s $true using if( $Sites ..) (not If( $Sites -eq $true ..)

    3) In Get-PriviledgedGroupsMemberCount (PS: it’s privileged)

    $PriviledgedGroups = @(“S-1-5-32-544″;”S-1-5-32-548″;”S-1-5-32-549″;”S-1-5-32-551″;”$DomainSIDValue-519″;”$DomainSIDValue-518”)
    foreach ($PriviledgedGroup in $PriviledgedGroups) {
    $Source = New-Object DirectoryServices.DirectorySearcher(“LDAP://$DomainName”)
    $source.SearchScope = ‘Subtree’
    $source.PageSize = 100000
    $Source.filter = “(objectSID=$PriviledgedGroup)”

    Why not combine it in a single AD query using LDAP filters – this will make the AD people happy too 🙂

    4) In Get-ADSiteLink (example of redundant code, there are more ..):

    if ($Sites -ne $null) {
    foreach ($Site in $Sites) {
    $Object = New-Object -TypeName ‘PSObject’
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Domain’ -Value $Domain.FQDN
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Name’ -Value $($SiteLink.properties.item(“name”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Description’ -Value $($SiteLink.properties.item(“Description”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘ReplicationInterval’ -Value $($SiteLink.properties.item(“replinterval”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Site’ -Value $(($SiteLink.Properties.item(“sitelist”) -split ‘,’ -replace ‘CN=’,”)[0])
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘SiteCount’ -Value ($SiteLink.Properties.item(“sitelist”)).count
    $Object
    }
    } else {
    $Object = New-Object -TypeName ‘PSObject’
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Domain’ -Value $Domain.FQDN
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Name’ -Value $($SiteLink.properties.item(“name”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Description’ -Value $($SiteLink.properties.item(“Description”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘ReplicationInterval’ -Value $($SiteLink.properties.item(“replinterval”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Site’ -Value ”
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘SiteCount’ -Value ‘0’
    $Object
    }

    can be rewritten as:

    foreach ($Site in $Sites) {
    $Object = New-Object -TypeName ‘PSObject’
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Domain’ -Value $Domain.FQDN
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Name’ -Value $($SiteLink.properties.item(“name”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Description’ -Value $($SiteLink.properties.item(“Description”))
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘ReplicationInterval’ -Value $($SiteLink.properties.item(“replinterval”))
    if ($Sites -ne $null) {
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Site’ -Value $(($SiteLink.Properties.item(“sitelist”) -split ‘,’ -replace ‘CN=’,”)[0])
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘SiteCount’ -Value ($SiteLink.Properties.item(“sitelist”)).count
    }
    } else {
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘Site’ -Value ”
    $Object | Add-Member -MemberType ‘NoteProperty’ -Name ‘SiteCount’ -Value ‘0’
    }
    $Object
    }

    Keep up the good work!

  5. Michel de Rooij says:

    Just noticed, why loop through all the elements after splitting up the Sites from the SiteList property when you are not processing those values and only add the first one – not the $Site DN?
    ( $(($SiteLink.Properties.item(“sitelist”) -split ‘,’ -replace ‘CN=’,”)[0]) )
    What am I missing something here?

  6. Jeff Wouters says:

    Hi Michel,
    Sweet feedback! I’ll include most of them in the next version… do you mind some credits in the comments? 😉

    For the part where the Word/PDF file is generated I’m not responsible, but I’ll fwd your feedback to the guys that is are.

    I’m testing for the usage of the variable this way because I want to be consistent in checking for the usage of the variable, no matter the variable type.
    Not the most optimal way, but I prefer consistency.
    I am / will also be checking for the usage of variables that aren’t of the type [switch].

    Thanks for taking the time to write and post the feedback, much appreciated!

    Jeff.

  7. Jeff Wouters says:

    No reason, will update in the next release 🙂

  8. CarlWebster says:

    Michel,

    I am the guy whose code Jeff is using. I do all the hoops because some people would get a variable not defined on some of the switches. I have no idea why. In my testing, I could never reproduce the error but I heard from enough people having the issue I had to do something. Doing the two-step shuffle of:

    If($PDF -eq $Null)
    {
    $PDF = $False
    }

    If(!(Test-Path Variable:PDF))
    {
    $PDF = $False
    }

    “fixed” the issue for the people having the undefined variable issue.

    I am in the process of changing some of my code to include extra “()” in logic tests because some people using non English versions of Windows and Word and PoSH V4 are having an issue. For example:

    $Results = Get-Something -EA 0

    If($? -and $Results -ne $Null)
    {
    #successful retrieval with data
    }
    ElseIf($? -and $Results -eq $Null)
    {
    #successful retrieval but no data returned
    }
    Else
    {
    #oops something went wrong
    }

    For some people, they always get the “oops” message.

    I am having to change the code to:

    If($? -and ($Results -ne $Null))
    {
    #successful retrieval with data
    }
    ElseIf($? -and ($Results -eq $Null))
    {
    #successful retrieval but no data returned
    }
    Else
    {
    #oops something went wrong
    }

    When I add the extra pair of (), for those users, the code now works and they get data.

    Why is the extra pair of () needed? I have no clue, they should not be needed.

    I find myself in the position of having to create code that works for everyone all the time. Why some things do not work for a few percentage of users but works for everyone else is beyond my limited understanding.

    Jeff can attest to my very limited PowerShell knowledge and coding skills.

    http://carlwebster.com/where-to-get-copies-of-the-documentation-scripts/

    Thanks

    Carl Webster
    Citrix Technology Professional
    http://www.CarlWebster.com
    The Accidental Citrix Admin

  9. jinish kg says:

    Sorry, I am a fresher in scripting, How we can execute this script? by simply executing in powershell console or need to use any switch?

  10. Jeff Wouters says:

    Hi Jinish Kg,
    Don’t worry, everyone starts at the beginning…
    Download and save the script, open a command prompt, browse to the script, type: Get-Help AD-Health-Check-v1.0-signed.ps1
    Actually executing the script you can do with: .\AD-Health-Check-v1.0-signed.ps1
    To execute the script, you need to make sure that script execution is enabled, which you can verify with Get-ExecutionPolicy and change with Set-ExecutionPolicy.
    More information about execution policies: http://jeffwouters.nl/index.php/2011/11/powershell-and-the-exectution-policies-explained

    Jeff.

  11. Andrew Fitzgerald says:

    Excellent script, i will add this to my article about performing an AD health check http://www.networkangel.net/active-directory-health-check-tools

  12. Jeff Wouters says:

    Thank you for the compliment 🙂

  13. Marcel Bosch says:

    Hello Jeff,

    Is it possible to make this script running for Word 2016? For now it only supports 2013 and older.

Leave a Reply

Your email address will not be published. Required fields are marked *