I really don’t like using tools like Quest for several reasons, primary among them being that it isn’t portable, or installed by default. I started the AD without Quest segment some time ago and I wanted to continue it again, but in a new direction. I think making scripts that can work in the pipeline is a better approach over making functions that port to each new script. That said, I wanted to make a generic AD searcher that could be used without modification for most queries. After I created it, I found that I prefer it over the UI based tools for information gathering purposes, it really is pretty neat.
Let’s get started!
First, we need to create our variables. As with all of my recent scripts, I have placed all of my variables in the Param block so that I can modify them at run time. I really like this practice since I can now change the way the script works at run time and I can even use “tab complete” to remember what I was thinking if I run the script 7 months from now. Notice that I setup a “default” value for each of these parameters so that it doesn’t need to be provided for the script to run, you are simply modifying the way the script runs with each.
1 2 3 4 5 6 7 |
Param( [String]$ObjectType = "*", [Parameter(ValueFromPipeline=$true)][String]$SearchTerm = "*", [String]$SearchProperty = "cn", [String]$SearchOperator = "=", [Array]$PropertiesToLoad ) |
Notice that the only value that I have defined as “from the pipeline” is the search term. This is because I think it’s the only thing that might change if it is coming from the pipeline. I don’t think, for instance that you would have a single search string, but then search on different properties. As well, I have forced the “PropertiesToLoad” to be an array. This is because I will use the .count property later, and if only one property is provided then PowerShell will make the variable a string instead of an array of strings. Strings do not have the .count property.
Now, I wanted to script to work in the pipeline, and there are several semantics that must be used to do this “properly” the first is that you have a “begin”, “process” and “end” block. The begin block will be run once, it is where you can setup any persistent objects or other complete environment items. In my case, I don’t want to create a new .NET AD searcher for every single item in the pipeline, the one will serve for everything, so I create it in the “begin” block.
1 2 3 |
Begin{ $Searcher=New-Object System.DirectoryServices.DirectorySearcher $Searcher.PageSize = 1000 |
The page size of the searcher will break the search into chunks returning 1000 objects at a time. The end result of a search with 8000 users is the same, but AD actually limits the number of items that can be returned in a search (by default 1000) so in the background we will perform eight inquiries to get our one return.
Now, I wanted a way get back only certain data. What if I only want to send the name down the pipeline? What if I want a quick way to see users with a logon count of greater than 1000 and their user names? I decided to let the user pass an array of attribute names to be loaded and sent down the pipeline. More on this later, but for now I am going to load them all into the searcher. Again, this is only done once, not for every single item since it’s in the begin block.
1 2 3 4 5 6 |
If ($PropertiesToLoad -ne $Null){ ForEach ($Property in $PropertiesToLoad){ $Searcher.PropertiesToLoad.Add($Property.ToLower()) | Out-Null } } } |
Now, we can move to the “process” portion of the script. This will get executed for each item that enters the pipeline, or a single time if executing outside of the pipeline.
1 |
Process{ |
The first thing that we need to do is setup the search string for the AD object. I am going to base this whole thing off of the parameters we created at the start. Those familiar with AD search string will recognize this. This is also where the heart of the script resides.
1 2 |
$Searcher.Filter="(&(objectcategory=$ObjectType($SearchProperty$SearchOperator$SearchTerm))" $Results=$Searcher.FindAll() |
All this does is make a string that makes a typical, quick search. The default value of the parameters would may the string "(&(objectcategory=*)(cn=*))" and return every single AD object. Any modification of the parameters narrows it down from there. If the script is executed with –ObjectType user then the string would become "(&(objectcategory=user)(cn=*))" and return every single user. I think you probably get the idea. The second line simply executes the query and places the results in our variable with the same name.
For the last part of the script, I wanted to format the data for use in the most intelligent way. Basically, depending on what the user provides for the “PropertiesToLoad” parameter we will have the script give a slightly different output. I decided that if the user gave nothing, then we would send the native .NET object that the searcher returns, unmodified. That would allow the most open-ended use of the return down the pipeline because all of the properties are included. If the user gives a single property, then they are probably trying to send a single piece of information down the pipeline, and so we can return a basic array of strings. Finally, if they are providing several, specific pieces, then perhaps they are trying to gather information, and we can make an array of objects with a property for each item requested and provide it, this makes the output compatible with cmdlets like Out-Gridview.
To start off we will see if the user provided input for “PropertiesToLoad” and either show the direct results, or start some post processing. If we are going to start post processing we need to make our array of objects and all associated properties. If there was only one object loaded, we’ll convert the array of objects into an array of strings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
If ($PropertiesToLoad -ne $Null){ ForEach ($Result in $Results){ $Return = "" | Select-Object $PropertiesToLoad ForEach ($Property in $PropertiesToLoad){ $Return.$Property = [STRING]$Result.properties.$($Property.ToLower()) } If ($PropertiesToLoad.Count -eq 1){ $Return | %{$_.$PropertiesToLoad} }Else{ $Return } } }Else{ $Results } |
To break it down more in case that’s a bit confusing. The first step is to make an object called Return and give it the properties that the user wants. This is a quick, easy way to do just that.
1 |
$Return = "" | Select-Object $PropertiesToLoad |
Next we will loop through all of the properties that they would like to have and move it from our results to our return object. We have to do this so that we can pick and choose only the items the user wanted. Even though the native .NET result was limited to the properties we told it above, it will always contain certain things like the distinguished name. Making our own object removes those items and allows PowerShell to display it in a pretty manner.
1 2 |
If ($PropertiesToLoad.Count -eq 1){ $Return | %{$_.$PropertiesToLoad} |
Here we are checking to see if only one property was provided. If so then we will quickly convert the array of objects into an array of strings. This is the best way I know how to do that. We are simply sending the object down the pipeline and running a ForEach-Object (% is a built-in alias) and printing the property.
1 2 3 4 |
}Else{ $Return } } |
If there is more than one property then we will print the whole object.
That’s it. Once you start to play around with the script you will start to see all of the possibilities. The following code block has the entire thing as one with a help block for usage information. This block will make the script compatible with Get-Help as well!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
#.Synopsis # This is a quick and open-ended Active Directory searcher # #.Description # This script will perform an Active directory search based off of the criteria provided. It # is capabale of reading items from the pipeline and delivering items to the pipeline. The output # format is dependant on parameters provided. # # Authored by Ryan Witschger - http://Ryan.Witschger.net # #.PARAMETER ObjectType # This is the type of object to search for. Typical valid options are "user" and "computer"; however, # it will take any valid entry for "objectCategory". This limits the objects returned to this # type. The default for this option is "*" and will search for all objects. # #.PARAMETER SearchTerm # This parameter takes the query to search for including wilcards. Examples are "*Ryan*" or "Server1". # Any valid AD search term can be accpeted. The default search term is "*" and will search for all objects. # This is the only parameter that will be read from th pipeline. # #.PARAMETER SearchProperty # The is the property to search against. Examples are "cn" and "whenCreated". This will work for # any entry to your AD schema. the default entry is "cn" to allow a search for object names # #.PARAMETER SearchOperator # This paramter takes the logical operator to provide with the AD search. Examples are "=", "<=" and ">=". # You may use any valid AD search operator here. The default for this entry is "=" and will allow # for a wildcard search of a string. # #.PARAMETER PropertiesToLoad # A list, comma seperated, or all properties that you would like to return for the objects found. This # parameter also determines the type of output for the script. If not provided the default is to return # the .NET object typical provided by an AD searcher. A single entry (example "Name") will provide an # array of strings with that property. Finally, an array of properties will provide and array of objects # with those properties present. This output may be used in the pipeline. # #.EXAMPLE # Return all AD objects. # # ./Get-ADObject.ps1 # #.EXAMPLE # Return all users named Steve. # # ./Get-ADObject.ps1 -ObjectType user -SearchTerm "*Steve*" # #.EXAMPLE # Read computers from a file and return thier last logon time. # # Get-Content c:Computers.txt | Get-ADObject.ps1 -ObjectType computer -SearchProperty name -PropertiesToLoad lastlogon # #.EXAMPLE # Get local user profiles, lookup their AD properties and display them in Out-Gridview # # GCI c:users | % {$_.Name} | .Get-ADObject.ps1 -SearchProperty sAMAccountName -PropertiesToLoad GivenName, ln, initials, whenCreated | Out-Gridview Param( [String]$ObjectType = "*", [Parameter(ValueFromPipeline=$true)][String]$SearchTerm = "*", [String]$SearchProperty = "cn", [String]$SearchOperator = "=", [Array]$PropertiesToLoad ) Begin{ $Searcher=New-Object System.DirectoryServices.DirectorySearcher $Searcher.PageSize = 1000 If ($PropertiesToLoad -ne $Null){ ForEach ($Property in $PropertiesToLoad){ $Searcher.PropertiesToLoad.Add($Property.ToLower()) | Out-Null } } } Process{ $Searcher.Filter="(&(objectcategory=$ObjectType)($SearchProperty$SearchOperator$SearchTerm))" $Results=$Searcher.FindAll() If ($PropertiesToLoad -ne $Null){ ForEach ($Result in $Results){ $Return = "" | Select-Object $PropertiesToLoad ForEach ($Property in $PropertiesToLoad){ $Return.$Property = [STRING]$Result.properties.$($Property.ToLower()) } If ($PropertiesToLoad.Count -eq 1){ $Return | %{$_.$PropertiesToLoad} }Else{ $Return } } }Else{ $Results } } |