Hey, Scripting Guy! How Can I Locate Missing Attributes in Active Directory Domain Services?

Hey, Scripting Guy! How Can I Locate Missing Attributes in Active Directory Domain Services?

  • Comments 3
  • Likes

Hey, Scripting Guy! Question

Hey, Scripting Guy! Why do some guys seem to get away with anything at work? I mean just because they spend all their time drinking coffee with the boss does that mean they do not have to do any work? You say you need examples? Consider our Active Directory Domain Services (AD DS). The users that I create, the groups I create, and the computer accounts I create all have the attributes filled out properly. Joe the Vacuum man—nothing between the ears—when he actually creates an object, half the time, he does not even specify a value for the Sam Account Name, little on anything else. I don’t imagine you can write a script that will force my worthless co-worker to do things properly, but can you at least help me find the objects that are missing values, so I can go and clean up after this swivel stick?

- EC

SpacerHey, Scripting Guy! Answer

Hi EC,

So how do you really feel about Joe? Don’t hold back. Maybe you need to investigate some primal scream therapy to help you deal with this guy. But you know, my heart went out to you, and I had to fight back empathy tears as I read your heart-wrenching letter. You know, we have all been there: doing all the work, but getting none of the recognition. There was this one clown…sorry, it’s your turn on the couch.

Despite our best efforts as network administrators, it is inevitable that objects will be created in AD DS that will be missing values for attributes. Sometimes, these are rather benign, such as a user who has not populated their favorite flower, and at other times, the missing data can have significant operational impact. It can certainly be frustrating when you opens a user object in AD DS Users and Computers and is greeted with the following display:

Image of an empty user object

 

This generally leads to the e-mail messages I get at scripter@Microsoft.com asking me questions such as, “How can I tell who created a user in AD DS?” Honestly, I got that question three times this morning! The answer, in case you are interested, is turn on AD DS auditing. Here is a good article about that. By the way, if everyone is logging in as administrator, the administrator did it! We do not have a magic interface that can read your face through the monitor, and figure out who you really are. If you know the credentials, as far as the OS is concerned, you are that person. (Sorry, I used to do security at Microsoft during the Nimda, Code Red, and SQL Slammer days, and I am still a little touchy about security issues. This may be an article for another day.)

From an auditing and management perspective, it is nice to be able to query AD DS and find the objects that have missing values. To do this, we will write a script that searches user objects in a particular organizational unit in a specific domain and looks at the value of the selected attribute. If the attribute value is null or empty, we print out the distinguished name of the user object. The script we develop to do this is called SearchAdForMissingAttributeValue.ps1 and is shown here:

#Requires -version 2.0
$SearchAttribute = "HomeDirectory"
$DisplayAttribute = "DistinguishedName"
$SearchRoot = "ou=testou,dc=nwtraders,dc=com"
$filter = "ObjectCategory=user"
$ds = [adsiSearcher]$filter
$ds.SearchRoot = "LDAP://$SearchRoot"
$ds.findAll() | ForEach-Object `
 -BEGIN { $i = 0 ; "$filter missing $SearchAttribute value" } `
 -PROCESS `
  {
    IF([string]::isNullOrEmpty($_.properties.item($SearchAttribute)))
     {
      $_.Properties.item($DisplayAttribute)
      $i++
     } # end if
  } `
 -END { "There are $i missing the $SearchAttribute value" }

The way the script is written, we can look at any object, for any attribute, and for any organizational unit or domain as these values are exposed via variables. Let’s examine this script in detail.

In the SearchAdForMissingAttributeValue.ps1 script, we begin by including a tag that specifies that Windows PowerShell 2.0 is required. The only item in the code that actually requires Windows PowerShell 2.0 is the [adsiSearcher] type accelerator. If we needed to ensure that the script would also run on Windows PowerShell 1.0, we could replace that line of code with this line of code:

$ds = New-Object DirectoryServices.DirectorySearcher("$filter")

We will examine this in a few paragraphs. As a best practice, when you write code that you know uses a feature from Windows PowerShell 2.0, make sure you include the requires tag as seen here.

#Requires -version 2.0

When using the requires tag to limit the execution of your script to PowerShell 2.0, the tag must be the first noncommented line in your script (even though the line itself has the pound sign [#] at the beginning).

The next two lines of the script create and define two variables. The first is the $SearchAttribute variable, which will hold the ADSI attribute we wish to locate. This is the attribute whose value we are inspecting to ensure that it is populated. We chose this attribute by finding the actual attribute name we are interested in selecting. As the script is written, we are going to be searching the HomeDirectory attribute to see if it is populated. If it is not, we will print out the value of the DistinguishedName.

$SearchAttribute = "HomeDirectory"
$DisplayAttribute = "DistinguishedName"

Out of the hundreds of attribute values that an object in AD DS may possess, most of them are not mandatory. One of the few attributes that will always be populated is the DistinguishedName attribute. It is a good one to use when you wish to locate an object, because it will tell you both the name and the location of the object.

The next variable we need to populate is the $searchRoot variable. This contains the path to the object we will connect to for our query. In most cases it will be an organizational unit. At times, you will want to connect to the root of a domain so that you can search the entire domain. The syntax used here is a simple value assignment:

$SearchRoot = "ou=testou,dc=nwtraders,dc=com"

We now need to create our search filter. The filter syntax is similar to what we used to use in VBScripts that searched AD. We have an entire repository devoted to searching AD DS. We use the $filter variable to hold our filter. We are looking for objects that have the value of the ObjectCategory attribute set to user. In plain language, we are looking for users. This code is shown here.

$filter = "ObjectCategory=user"

Now we need to perform the search. To do this, we are going to use the System.DirectoryServices.DirectorySearcher to perform the search. In Windows PowerShell 2.0, we can access this .NET Framework class by using the [adsisearcher] type accelerator. In Windows PowerShell 1.0, we would need to write the code as seen here:

$ds = New-Object DirectoryServices.DirectorySearcher("$filter")

As you can see, it is not all that hard to create an instance of the DirectorySearcher class. But when you compare the line of code above, with the one listed here that uses Windows PowerShell 2.0 syntax, it does save a bit of typing:

$ds = [adsiSearcher]$filter

We need to define the search root that will be used for the DirectorySearcher. The SearchRoot attribute is used to control where the search will begin. In addition to specifying the location of the search, we also use the SearchRoot attribute to specify the protocol that will be used. In this example, we use the LDAP protocol to search Active Directory, and we will connect to the location that is stored in the $SearchRoot variable. This is shown here:

$ds.SearchRoot = "LDAP://$SearchRoot"

It is now time to execute the search. We use the findall method to return all the items that match our search filter. Because it is possible that the query could return a large number of items, we decide to pipeline the objects rather than storing them in a variable. In addition to reducing the amount of memory required for the script, it also has the advantage of being quicker by allowing items to flow into the pipeline for further processing, rather than requiring everything to be returned and stored in a variable before processing. This line of code, including the pipe character, is shown here.

$ds.findAll() |

For each item that comes down the pipeline, we are going to do something. When we are piping information, we use the ForEach-Object cmdlet. If we were storing information in a variable and we wanted to iterate through the collection, we would use the ForEach statement. There are advantages to the ForEach-Object cmdlet, however, in that we have a number of parameters we can use to simplify our code and add flexibility. The only thing to keep in mind is that it is a single command; as a result, we will need to use the backtick (`) character to allow us to break the command onto several lines. That will make the code easier to read. We begin with the ForEach-Object cmdlet and a line continuation character as shown here:

ForEach-Object `

Now we want to do something before we actually start working with our data as it streams down the pipeline. To enable us to do this, we use the -BEGIN parameter. What we want to do is to initialize a variable that will be used for counting the items we find. As a best practice, I recommend initializing these kinds of “throwaway” variables close to the point of use. When you use this technique, it can aid in limiting the variables' scope to the localized procedure. When the variable $i is initialized, we then print out a header for our output that indicates the search attribute used to produce the results. We are not done with the ForEach-Object cmdlet, so we end the command with a back tick as seen here.

-BEGIN { $i = 0 ; "$filter missing $SearchAttribute value" } `

We next want to begin the actual processing of the data that comes down the pipeline. To do this, we use the –PROCESS parameter and trail it with a back tick as shown here.

-PROCESS `  {

Now we need to find out if the value of the search attribute is empty or null. To do this, we use the static isNullOrEmpty method from the system.string class. We use the If statement to do the evaluation. As a best practice, when evaluating something that returns a Boolean value such as the isNullOrEmpty method, use the Boolean directly instead of trying to evaluate it as true of false, or 0 or -1. We use the $_ automatic variable to refer to the current object on the pipeline. We use the properties property to return a collection of properties, and use the item method to retrieve the specific AD DS attribute we are interested in. This line of code is seen here:

IF([string]::isNullOrEmpty($_.properties.item($SearchAttribute)))

If we do find an attribute with an empty value, we want to display the attribute we selected via the $DisplayAttribute variable. We then print that value out on the line and increment the value of the $i variable. This code is seen here:

      {
      $_.Properties.item($DisplayAttribute)
      $i++
     } # end if

When we have worked our way through all the items returned by the DirectorySearcher, we would like to display a summary that informs us how many items were found that match our $SearchAttribute value. To do this, we use the –END parameter from the ForEach-Object cmdlet as seen here:

} ` -END { "There are $i missing the $SearchAttribute value" }

When we run the script, we are greeted with this output:

Image of an empty user object

 

EC, we have come up with a solution for identifying your missing attributes. Unfortunately, we do not have a .NET Framework class called system.workplace.UselessCoWorker, and therefore we cannot find the DoSomeWorkAlready method. But hopefully this will help. See you tomorrow.

Ed Wilson and Craig Liebendorfer, Scripting Guys

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • This script wont work if you dont change the "$ds.findAll() | Object `" line to "$ds.findAll() | ForEach-Object `" Without the quotes of course.  if you walk through the whole blog you will see that ScriptingGuy does mention the correct cmdlet, but in the complete SearchAdForMissingAttributeValue.ps1 shown at the top it just has Object which will result in the following error message "The term 'Object' is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again."

    just an FYI.  also, thanks for this handy search script. love it and use it all the time. :)

  • @Skillionaire, thanks for pointing the error out, I do not know how the ForEach got omitted. I have corrected the code in the blog post.

  • Hi, I've been struggling to add another attribute (i.e. department) to appear in the output, however with my "monkey-see monkey-do" skills I could not get beyond outputting the department below each username entry with the below code, any assistance to list multiple attributes per username/object in a single line?

    #Requires -version 2.0

    $SearchAttribute = "ipPhone"

    $DisplayAttribute = "cn"

    $DisplayAttribute2 = "department"

    $SearchRoot = "OU=mydomain Co.,DC=mydomain,DC=net"

    $filter = "ObjectCategory=user"

    $ds = [adsiSearcher]$filter

    $ds.SearchRoot = "LDAP://$SearchRoot"

    $ds.findAll() | foreach-Object `

    -BEGIN { $i = 0 ; "$filter missing $SearchAttribute value" } `

    -PROCESS `

     {

       IF([string]::isNullOrEmpty($_.properties.item($SearchAttribute)))

        {

        $_.Properties.item($DisplayAttribute) + $_.Properties.item($DisplayAttribute2)

         $i++

        } # end if

     } `

    -END { "There are $i missing the $SearchAttribute value" }