HOWTO: Easily Compare AD Attributes Between Users

You guys are in for a treat today.  Have you ever attempted to troubleshoot an issue with an Active Directory user only to find yourself in ADSI edit trying to figure out if any attributes are not configured correctly?  Since you’re often not sure what the value is supposed to be, you’d like to compare it to another known working user. That’s a pretty common problem in Windows IT and I’ve just created a tool that makes it much, much easier to do!  Let me know show you how it works.

When you run this tool, a dialog box appears that looks like this:

image

  • This screen is asking for the users you’d like to display Active Directory attributes for.  You can enter 1 or 2 or 10 different users if you’d like, just be sure to separate each one with a semicolon.
  • It can take a few moments for the AD attributes to load so a progress bar is displayed.  It shouldn’t take more than 5 seconds or so per user.

image

  • You’re now presented with a grid view that contains 3 columns : the username, the attribute name and its value. That’s pretty cool, but that’s not the best part.  The grid view has a built in real time search that automatically searches every single item of text means that you can do some incredibly powerful analytics.

image

  • Let’s say for example that you are troubleshooting a logon issue for user named sbrown.  You know that jack.johnson is working.  You also want to include your own account as a control.
  • All you need to do is in the filter box type logon.  This will return any field in any attribute or description or name that has that key word

image

  • Ah ha, in this case we can see that sbrown has never actually logged in before.  This tool makes it incredibly simple to analyze Active Directory attributes.
  • Here is another example.  Let’s say you’re having email issues with two users.  You know your email works so you compare them and search for the email domain name:

image

  • Ah ha!  The working account has a proxyAddress configured while the two accounts that are not working do not.  (This is because in my lab only admin-rv is provisioned with an Exchange account).

Lastly, let’s say you want to export all of the attributes related to passwords into a spreadsheet for further analysis, reporting, etc?  Turns out all you need to do is select the records you want and press OK

image

The results are automatically pasted into your clipboard.  You can now paste these results into Excel for example.

image

The sky is the limit!

Here is the script:

Accepts one or more user names and displays all AD attributes for those users in a GridView for easy analysis
# Home; Active Directory; Attributes; Users; Analysis;

# Requires PowerShell 3 due to how the type accelerator I use to generate custom objects
#requires -version 3
# Also requires that the module "ActiveDirectory" is loaded to use Get-ADUser

# Required to load the Messagebox to display a friendly error if no user is found
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null

Function Get-UserInput([string]$Description)
{
    $objForm = New-Object System.Windows.Forms.Form 
    $objForm.Text = "AD User Attributes"
    $objForm.Size = New-Object System.Drawing.Size(300,170) 
    $objForm.StartPosition = "CenterScreen"

    $objForm.KeyPreview = $True
    $objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter") 
        {$x=$objTextBox.Text;$objForm.Close()}})
    $objForm.Add_KeyDown({if ($_.KeyCode -eq "Escape") 
        {$objForm.Close()}})

    $OKButton = New-Object System.Windows.Forms.Button
    $OKButton.Location = New-Object System.Drawing.Size(75,95)
    $OKButton.Size = New-Object System.Drawing.Size(75,23)
    $OKButton.Text = "OK"
    $OKButton.Add_Click({$x=$objTextBox.Text;$objForm.Close()})
    $objForm.Controls.Add($OKButton)

    $CancelButton = New-Object System.Windows.Forms.Button
    $CancelButton.Location = New-Object System.Drawing.Size(150,95)
    $CancelButton.Size = New-Object System.Drawing.Size(75,23)
    $CancelButton.Text = "Cancel"
    $CancelButton.Add_Click({$objTextBox.text = "Cancel"; $objForm.Close()})
    $objForm.Controls.Add($CancelButton)

    $objLabel = New-Object System.Windows.Forms.Label
    $objLabel.Location = New-Object System.Drawing.Size(10,20) 
    $objLabel.Size = New-Object System.Drawing.Size(280,40) 
    $objLabel.Text = $Description
    $objForm.Controls.Add($objLabel) 

    $objTextBox = New-Object System.Windows.Forms.TextBox 
    $objTextBox.Location = New-Object System.Drawing.Size(10,60) 
    $objTextBox.Size = New-Object System.Drawing.Size(260,20)
    # Include default usernames here if desired
    $objTextBox.Text = "samaccountname" 
    $objForm.Controls.Add($objTextBox) 

    $objForm.Topmost = $True

    $objForm.Add_Shown({$objForm.Activate(); $objTextBox.focus()})
    [void] $objForm.ShowDialog()

    return $objtextBox.Text
}

# Create a custom object that will store the values we discover to present them in a gridview
$C="UserName Attribute Value"; $myobj=@(); Function Add-ToObject{$args|%{$i++;$P+=@{$C.split(" ")[$i-1]=$_}};$Script:myObj+=@([pscustomobject]$P)}

$UserNames = Get-UserInput "Enter samaccountname(s) to show AD user attributes (Separate each name with a `";`")"
if($UserNames -eq "Cancel" -or $UserNames -eq '') { break } 
# If more than one user is found, split them out into separate elements so they can be checked one at a time
$UserNames = $UserNames -split ";"

# This code can probably be combined with the other checks as we are performing multiple redundant get-aduser calls but this works for now
# The purpose of this loop is simply to validate that the users exist and if not, display an error message, and quit
ForEach($Username in $UserNames)
{
    try{
        Get-ADUser -Identity $UserName -ErrorAction SilentlyContinue | Out-Null
    }
    catch{
        [Windows.Forms.MessageBox]::Show("Cannot find user `'$Username`'.  `nExiting Tool." , "AD User Attributes", [Windows.Forms.MessageBoxButtons]::OK , [Windows.Forms.MessageBoxIcon]::Error) | Out-Null; Exit
    }
}
cls

# We need to enumerate all Active Directory propeties so we can later expand each of them and store any entries with multiple line or objects as a single text string
# Different users can have different attributes so we need to first build a list of all possible attributes to later iterate through
$UserNames | % { Write-Progress -Activity "Reading active directory attributes for $UserName..."; $Props += (Get-ADUser $_ -Properties * | Get-Member -MemberType Properties)  }

# Once we have a master list of all properties, generate a list of all of the unique ones, eliminating the duplicates
$Props = $Props | select -Unique

ForEach($Username in $UserNames) 
{
    # Grab all attributes and values from Active Directory for this user
    $UserDetails = get-aduser $UserName -Properties * -ErrorAction Stop

    ForEach ($UserDetail in $UserDetails)
    {
        ForEach($Prop in $Props)
        {
            Write-Progress -Activity "Reading active directory attributes for $UserName..." -Status $Prop
            
            # For properties with multiple values, expand them and append them to a single line separated by a semicolon so Out-GridView can search it
            $Result = (get-aduser $UserName -Properties * | select -ExpandProperty $Prop.Name) -join ";"
            
            # The attributes defined below are known to be stored in a unique format.  We need to confirm them to a human readable form using the .NET method ::Fromfiletime
            switch ($Prop.name) {
                'lastLogonDate' { if(!$Result) { $Result = "Never Logged In" } }

                { 
                    'lastLogonTimestamp',
                    'lastLogon',
                    'badPasswordTime',
                    'lastLogonTimestamp',
                    'pwdlastset' -contains $_ } { if($Result) { $Result = [string][datetime]::fromfiletime($Result) } 
                }
                # If a password is marked to never expire, it is assigned the value below.  if we find that, we translate that into human readable form.  Otherwise we convert it
                'accountExpires' { if ($Result -eq "9223372036854775807") {$Result = "Never Expires" } Else { $Result = [string][datetime]::fromfiletime($Result) } }
            }
            
            Add-ToObject $UserName $Prop.Name $Result
        }
    }
}

# Whatever is selected inside the grid view is automatically exported to the clipboard as CSV data
$SendToClipboard = $myobj | select UserName, Attribute, Value | Out-GridView -Title "Active Directory User Attributes" -PassThru | ConvertTo-CSV -NoTypeInformation | clip

13 comments

Skip to comment form

    • pn on March 1, 2017 at 9:50 am
    • Reply

    ” is replaced by " in the script. You can use search and replace to fix it.

    • acatic on August 18, 2017 at 3:47 pm
    • Reply

    Sounds promising. I copied the whole code into a .ps1 file, replaced all ampersands with “&” as Powershell instructs me, imported activedirectory, tried running the script with my domain admin creds, but it just spat out dozens of these errors:
    – “Unexpected token ‘quot’ in expression or statement.”
    – “Missing closing ‘)’ after expression in ‘if’ statement.”
    -“Missing closing ‘}’ after expression in ‘if’ statement.”
    -“Missing condition in switch statement clause.”
    Thoughts?
    Thanks for all guidance!

  1. Hi Acatic,

    Grrr, WordPress did screw up the formatting on the quotes. I’ll go fix it later. But for now, I pasted the script into my production environment, fixed the quotes issue and ran it again and this time it worked as expected. Try copying and pasting this and see if you have any better luck:

    # Accepts one or more user names and displays all AD attributes for those users in a GridView for easy analysis
    # Home; Active Directory; Attributes; Users; Analysis;

    # Requires PowerShell 3 due to how the type accelerator I use to generate custom objects
    #requires -version 3
    # Also requires that the module “ActiveDirectory” is loaded to use Get-ADUser

    # Required to load the Messagebox to display a friendly error if no user is found
    [System.Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”) | Out-Null

    Function Get-UserInput([string]$Description)
    {
    $objForm = New-Object System.Windows.Forms.Form
    $objForm.Text = “AD User Attributes”
    $objForm.Size = New-Object System.Drawing.Size(300,170)
    $objForm.StartPosition = “CenterScreen”

    $objForm.KeyPreview = $True
    $objForm.Add_KeyDown({if ($_.KeyCode -eq “Enter”)
    {$x=$objTextBox.Text;$objForm.Close()}})
    $objForm.Add_KeyDown({if ($_.KeyCode -eq “Escape”)
    {$objForm.Close()}})

    $OKButton = New-Object System.Windows.Forms.Button
    $OKButton.Location = New-Object System.Drawing.Size(75,95)
    $OKButton.Size = New-Object System.Drawing.Size(75,23)
    $OKButton.Text = “OK”
    $OKButton.Add_Click({$x=$objTextBox.Text;$objForm.Close()})
    $objForm.Controls.Add($OKButton)

    $CancelButton = New-Object System.Windows.Forms.Button
    $CancelButton.Location = New-Object System.Drawing.Size(150,95)
    $CancelButton.Size = New-Object System.Drawing.Size(75,23)
    $CancelButton.Text = “Cancel”
    $CancelButton.Add_Click({$objTextBox.text = “Cancel”; $objForm.Close()})
    $objForm.Controls.Add($CancelButton)

    $objLabel = New-Object System.Windows.Forms.Label
    $objLabel.Location = New-Object System.Drawing.Size(10,20)
    $objLabel.Size = New-Object System.Drawing.Size(280,40)
    $objLabel.Text = $Description
    $objForm.Controls.Add($objLabel)

    $objTextBox = New-Object System.Windows.Forms.TextBox
    $objTextBox.Location = New-Object System.Drawing.Size(10,60)
    $objTextBox.Size = New-Object System.Drawing.Size(260,20)
    # Include default usernames here if desired
    $objTextBox.Text = “samaccountname”
    $objForm.Controls.Add($objTextBox)

    $objForm.Topmost = $True

    $objForm.Add_Shown({$objForm.Activate(); $objTextBox.focus()})
    [void] $objForm.ShowDialog()

    return $objtextBox.Text
    }

    # Create a custom object that will store the values we discover to present them in a gridview
    $C=”UserName Attribute Value”; $myobj=@(); Function Add-ToObject{$args|%{$i++;$P+=@{$C.split(” “)[$i-1]=$_}};$Script:myObj+=@([pscustomobject]$P)}

    $UserNames = Get-UserInput “Enter samaccountname(s) to show AD user attributes (Separate each name with a ";“)”
    if($UserNames -eq “Cancel” -or $UserNames -eq ”) { break }
    # If more than one user is found, split them out into separate elements so they can be checked one at a time
    $UserNames = $UserNames -split “;”

    # This code can probably be combined with the other checks as we are performing multiple redundant get-aduser calls but this works for now
    # The purpose of this loop is simply to validate that the users exist and if not, display an error message, and quit
    ForEach($Username in $UserNames)
    {
    try{
    Get-ADUser -Identity $UserName -ErrorAction SilentlyContinue | Out-Null
    }
    catch{
    [Windows.Forms.MessageBox]::Show(“Cannot find user '$Username‘. `nExiting Tool.” , “AD User Attributes”, [Windows.Forms.MessageBoxButtons]::OK , [Windows.Forms.MessageBoxIcon]::Error) | Out-Null; Exit
    }
    }
    cls

    # We need to enumerate all Active Directory propeties so we can later expand each of them and store any entries with multiple line or objects as a single text string
    # Different users can have different attributes so we need to first build a list of all possible attributes to later iterate through
    $UserNames | % { Write-Progress -Activity “Reading active directory attributes for $UserName…”; $Props += (Get-ADUser $_ -Properties * | Get-Member -MemberType Properties) }

    # Once we have a master list of all properties, generate a list of all of the unique ones, eliminating the duplicates
    $Props = $Props | select -Unique

    ForEach($Username in $UserNames)
    {
    # Grab all attributes and values from Active Directory for this user
    $UserDetails = get-aduser $UserName -Properties * -ErrorAction Stop

    ForEach ($UserDetail in $UserDetails)
    {
    ForEach($Prop in $Props)
    {
    Write-Progress -Activity “Reading active directory attributes for $UserName…” -Status $Prop

    # For properties with multiple values, expand them and append them to a single line separated by a semicolon so Out-GridView can search it
    $Result = (get-aduser $UserName -Properties * | select -ExpandProperty $Prop.Name) -join “;”

    # The attributes defined below are known to be stored in a unique format. We need to confirm them to a human readable form using the .NET method ::Fromfiletime
    switch ($Prop.name) {
    ‘lastLogonDate’ { if(!$Result) { $Result = “Never Logged In” } }

    {
    ‘lastLogonTimestamp’,
    ‘lastLogon’,
    ‘badPasswordTime’,
    ‘lastLogonTimestamp’,
    ‘pwdlastset’ -contains $_ } { if($Result) { $Result = [string][datetime]::fromfiletime($Result) }
    }
    # If a password is marked to never expire, it is assigned the value below. if we find that, we translate that into human readable form. Otherwise we convert it
    ‘accountExpires’ { if ($Result -eq “9223372036854775807”) {$Result = “Never Expires” } Else { $Result = [string][datetime]::fromfiletime($Result) } }
    }

    Add-ToObject $UserName $Prop.Name $Result
    }
    }
    }

    # Whatever is selected inside the grid view is automatically exported to the clipboard as CSV data
    $SendToClipboard = $myobj | select UserName, Attribute, Value | Out-GridView -Title “Active Directory User Attributes” -PassThru | ConvertTo-CSV -NoTypeInformation | clip

    • AndrewM on September 7, 2017 at 3:09 pm
    • Reply

    I’m still getting errors when trying this on my DC – Copied the above text (in replied post not orig.) – Not too sure what the issue is 0 Do I need to run this under “AD Module for windows powershell”?

    PS C:\Users\bigadmin> C:\Users\bigadmin\Desktop\ADattributecomapte.ps1
    At C:\Users\bigadmin\Desktop\ADattributecomapte.ps1:65 char:33
    + $UserNames = $UserNames -split “;”
    + ~
    Missing closing ‘)’ after expression in ‘if’ statement.
    At C:\Users\bigadmin\Desktop\ADattributecomapte.ps1:75 char:35
    + [Windows.Forms.MessageBox]::Show(“Cannot find user ‘$Username‘. `nExiting Tool.” …
    + ~~~~~~
    Unexpected token ‘Cannot’ in expression or statement.
    At C:\Users\bigadmin\Desktop\ADattributecomapte.ps1:103 char:45
    + ‘lastLogonDate’ { if(!$Result) { $Result = “Never Logged In” } }
    + ~~~~~
    Unexpected token ‘Never’ in expression or statement.
    At C:\Users\bigadmin\Desktop\ADattributecomapte.ps1:122 char:118
    + … User Attributes” -PassThru | ConvertTo-CSV -NoTypeInformation | clip
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    The string is missing the terminator: “.
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : MissingEndParenthesisAfterStatement

      • AndrewM on September 7, 2017 at 3:11 pm
      • Reply

      Forgot to mention I’m logged in as Domain Admin, so permissions shouldnt be an issue.

  2. Hi Andrew,

    So something odd is going on with my WordPress rending it seems that I’m going to have to investigate. I copied and pasted the code above and tried running it and got the same error as you. The issue is on line 63 as a quote is removed for some unknown reason.

    On line 63, add a second quote to the $Usernames -eq expression and try running it again.

    if($UserNames -eq “Cancel” -or $UserNames -eq “”) { break }

    Once I made that change it worked for me. Try that and let me know if that helps.

    It looks like I’m going to have to do some troubleshooting on my website with my PowerShell rendering plugins. 🙁

  3. I have noticed you don’t monetize your blog, don’t waste your traffic, you can earn additional bucks every
    month because you’ve got high quality content.
    If you want to know how to make extra $$$, search for:
    Boorfe’s tips best adsense alternative

    • ThatOneITGuy on February 13, 2019 at 10:24 pm
    • Reply

    Just wanted to drop a note and say that this script helped me troubleshoot an Exchange related gremlin that was about to make me start drinking heavily. Kudos!

    1. Glad to hear it helped you out. I know it’s saved me from punching something numerous times. 🙂

    • Oliver Richter on December 11, 2020 at 3:38 pm
    • Reply

    Hi,

    nice script – but how can it run?

    Like *.vbs file with cscript?

    Actually it is not working for me.

    • Robert on January 20, 2021 at 10:29 am
    • Reply

    Thank you!
    It helped me to very quickly diagnose problem in my system!

    • Dr Andy on March 10, 2021 at 4:07 pm
    • Reply

    this only works on users? would like to be able to compare some distribution lists, but getting the response: “Cannot find user ‘blahblah’. Existing tool”

    • Mark Latham on April 22, 2021 at 7:13 pm
    • Reply

    Do you have a link to download it? I have so many errors trying to run it on say 20 lines – Or another site you have posted it to. Thanks!

Leave a Reply to AndrewM Cancel reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.