Tag Archives: Get-ADUser

A Basic(ish) Active Directory Look-Up Script

It was just Saturday—it’s been a few weeks now, actually—that I wrote a post proclaiming to be back. Back to writing about PowerShell, that is. Why not take the first script I wrote in my new position and share it? It’s possible it has some concepts that might be helpful for readers.

The first thing I want to mention is that I hate writing scripts. Huh!? What I mean by that, is that I prefer to write functions. That didn’t happen my first time out in this position, as you’ll see, and I’m going to be okay with it. A one-off script may have a purpose. I stayed away from a function (and therefore a module [maybe], working with profile scripts [potentially], etc.). I’ll live with it.

Let’s break the script up into sections and explain what’s happening in each. Keep in mind that this isn’t perfect and there are places where I would make changes after having looked over this a couple of times. I’ll be sure to mention those toward the bottom of the post. Okay, let’s do this! I’ll put the script back together into a single, code block further below.

The beginning of the script creates two parameters: Properties and Output. Properties can accept multiple string values,—notice the []—and we’ll see that in the upcoming examples. Output can accept one string value of three predetermined values. Those are Console, GridView, and CsvFile; Console is the default parameter value.

Param (
    [Parameter()]
    [string[]]$Properties,
    [Parameter()]
    [ValidateSet('Console','GridView','CsvFile')]
    [string]$Output = 'Console'
)

Next, we create a path to a flat file called userlist.txt. This file will contain Active Directory Display Names (DisplayName). By using the $PSScriptRoot variable, all we have to do is keep our script and the text file in the same location/folder in order for it to work correctly.

Once the $Path variable is set, we attempt to run a Get-Content command against the values in the file, storing these in the $Userlist variable. If for some reason the file isn’t in place, the script will make use of the catch block of our try-catch to indicate to the user that the file can’t be located.

$Path = "$PSScriptRoot\userlist.txt"
try {
    $Userlist = Get-Content -Path $Path -ErrorAction Stop
} catch {
    Write-Error -Message "Unable to locate $Path."
}

Following that, we set our $TotalProperties variable to three Active Directory user properties we know we want. Then, if any values have been passed in using the Properties parameter, we combine those with the three properties already in the $TotalProperties variable.

$TotalProperties = @('DisplayName','EmployeeNumber','SamAccountName')
if ($Properties) {
    $TotalProperties = $TotalProperties + $Properties
}

Moving forward, we set up foreach language construct in order to loop through each user in the $Userlist variable. Remember, this variable was populated by our Get-Content command earlier. For each loop iteration, we are adding a PSCustomObject to our $Users—plural— variable. Normally, I wouldn’t store each value in a variable and instead just pump it out right there, but the script includes some output options that we’ll see next.

foreach ($User in $Userlist) {
    $Users += [PSCustomObject]@(
    Get-ADUser -Filter "DisplayName -eq '$User'" -Properties $TotalProperties |
        Select-Object -Property $TotalProperties
    )
}

Finally, we consider the output option the user either defaulted to, or didn’t. If they included the Output parameter with GridView, we pipe our $Users variable to OutGridView. If they included the Output parameter with CSVFile, we pipe out $Users variable to Export-Csv, saving them in a CSV file, and then open our saved CSV file. If they didn’t include the Output parameter, or they did with the Console value, then we display the results directly in the console. That’s it.

Switch ($Output) {
    GridView {$Users | Out-GridView -Title Users}
    CsvFile {
        $Users.GetEnumerator() |
        Export-Csv -NoTypeInformation -Path "$PSScriptRoot\$(Get-Date -Format FileDateTime -OutVariable NewFileOutput).csv"
        Invoke-Item -Path "$PSScriptRoot\$NewFileOutput.csv"
    }
    Default {$Users | Format-Table -AutoSize}
}

Although I took the base code from someone’s previously written script, this really is still much of a 1.0.0 version. Knowing that, there are some changes I might make; it’s not perfect, but it’ll get the job done.

  • While it’s not vital, I kind of wish I used a different variable name for $Path
    • It’s a path, sure, but to a specific file
    • Perhaps $FilePath, $UserFile, or $UserFilePath
      • It could’ve been more specific
  • Ensure properties passed in via the Properties parameter are valid for an Active Directory user
    • If someone sends in properties that don’t exist for an Active Directory user, it’s going to cause problems
      • Maybe check a known user, gather all the possible properties, and compare
      • (Or) Maybe wrap some error checking without having to do any property compare operation
  • Don’t use Default in the Switch language construct
    • It’s not necessary, as the Output parameter will only accept three possible values
    • Default could’ve been replaced with Console

Here are a few examples followed by the full PowerShell code in a single, code block.

I’ve redacted information from each of these images. There’s something that’s vital to know about each, however. In front of the full path (all the images but the last one), is the & operator. This is called the invocation or call operator. It informs PowerShell that everything after it should be treated as a command and that it’s not just a long, string value.

This example invokes the script without any parameters, pulling in two users from the userlist.txt file.

This example invokes the script and includes two additional Active Directory properties, which are then also included in the output.

This example does the same as the first one, however, it opens the results using the Out-GridView cmdlet.

This one opens the results using whatever program—Excel in my case—is associated with CSV files. This option is saving the file to disk, so keep that in mind, as it has no cleanup features.

This final example is included to show that it works when you’re inside the same directory as the script and text file. It also includes multiple parameters being included at the same time. You might know you can do it, but my at-work audience may not have—I’m not sure. As we’re in the directory with the script, you can actually see the inclusion of the invocation operator.

And finally, all the code in a single code block.

Param (
    [Parameter()]
    [string[]]$Properties,
    [Parameter()]
    [ValidateSet('Console','GridView','CsvFile')]
    [string]$Output = 'Console'
)

$Path = "$PSScriptRoot\userlist.txt"
try {
    $Userlist = Get-Content -Path $Path -ErrorAction Stop
} catch {
    Write-Error -Message "Unable to locate $Path."
}

$TotalProperties = @('DisplayName','EmployeeNumber','SamAccountName')
if ($Properties) {
    $TotalProperties = $TotalProperties + $Properties
}

foreach ($User in $Userlist) {
    $Users += [PSCustomObject]@(
        Get-ADUser -Filter "DisplayName -eq '$User'" -Properties $TotalProperties |
            Select-Object -Property $TotalProperties
    )
}

Switch ($Output) {
    GridView {$Users | Out-GridView -Title Users}
    CsvFile {
        $Users.GetEnumerator() |
            Export-Csv -NoTypeInformation -Path "$PSScriptRoot\$(Get-Date -Format FileDateTime -OutVariable NewFileOutput).csv"
        Invoke-Item -Path "$PSScriptRoot\$NewFileOutput.csv"
    }
    Default {$Users | Format-Table -AutoSize}
}

Read-Only and Constant Variables


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on September 23, 2019.


Quick reminder: You can make variables in PowerShell read-only, or constant.

I’ve written about this before, but it’s time to cover it again for any newcomers to PowerShell, and because I actually found a need for it. Previously, it was just pure learning and sharing. Today, I’ll let you in on how I recently used a read-only variable, and bonus, something else I discovered, too! Let’s begin with how to use these variable options.

When you’re new to PowerShell, you learn to create variables by using the assignment operator. No, not the equals sign. Look at the below example, and then say it out loud as, “ADUser (or dollar sign ADUser) is assigned the values returned by the Get-ADUser Cmdlet.” Don’t say equals; it’s not equals. Lose that habit, if you have it. And yes, people will be listening for it. Okay fine. It may just be me.

PS C:\> $ADUser = Get-ADUser -Identity tommymaynard

You can also create a variable and assign it a value by using the New-Variable Cmdlet. Notice that we don’t use a dollar sign when we do it this way. The same goes for all of the *-Variable cmdlets (Get-Command -Name *-Variable). Dollar signs aren’t a part of a variable, as much as they’re there to indicate to the parser, that what follows a dollar sign, is a variable. That’s how I’ve come to understand it, anyway.

PS C:\> New-Variable -Name User -Value tommymaynard
PS C:\> $User
tommymaynard
PS C:\> $User.GetType().Name
String
PS C:\> Get-Variable -Name User

Name                           Value
----                           -----
User                           tommymaynard

We can use Set-Variable to reassign our previously existing variable. If we use Set-Variable against a variable that doesn’t already exist, it essentially runs New-Variable. It may actually run New-Variable.

PS C:\> Set-Variable -Name User -Value 'maynard, tommy'
PS C:\> $User
maynard, tommy
PS C:\>

Now, one last piece of information before we move on. New-Variable (as well as Set-Variable), has an Option parameter. It’ll accept a handful of predetermined arguments or parameter values, but today we’ll cover the ReadOnly value. We’ll touch on the Constant value, too. A read-only variable is one where you can’t change the value after the variable has been assigned. Okay, that’s not entirely true. You can reassign a read-only variable if you use the Force parameter. If we use Constant instead of ReadOnly for the Option parameter’s value then there would be no way to reassign the variable ever. Additionally, you can only make a variable a constant when it’s first created. That’s pretty much the difference between those two.

PS C:\> New-Variable -Name Var -Value 'testing' -Option ReadOnly
PS C:\> Set-Variable -Name Var -Value 'not testing'
Set-Variable : Cannot overwrite variable Var because it is read-only or constant.
At line:1 char:1
+ Set-Variable -Name Var -Value 'not testing'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : WriteError: (Var:String) [Set-Variable], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable,Microsoft.PowerShell.Commands.SetVariableCommand
PS C:\> $Var = 'not testing'
Cannot overwrite variable Var because it is read-only or constant.
At line:1 char:1
+ $Var = 'not testing
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo          : WriteError: (Var:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable
PS C:\> $Var
testing
PS C:\> Set-Variable -Name Var -Value 'not testing' -Force
PS C:\> Get-Variable -Name Var

Name                           Value
----                           -----
Var                            not testing

PS C:\> (Get-Variable -Name Var).Value
not testing
PS C:\>

I have what I’ve written and called, the Advanced Function Template, for work. I won’t go into all the neat things it includes, but a few years later and I’m still quite proud of all the things I’ve stuffed into it. I use it as my starting point for every function I author. One thing it does is logging (to the screen, to a file, or both). As a part of its logging, it lists the block location. Here’s an example of a function created with the template (that has no, non-template code logging, at minimum).

VERBOSE: [INFO   ] Invoking the Get-WorkDomainComputer function.
VERBOSE: [INFO   ] Invoking user is "MYDOMAIN\tommymaynard."
VERBOSE: [INFO   ] Invoking on Saturday, September 14, 2019 9:47:12 PM.
VERBOSE: [INFO   ] Invoking on the "TOMMAY-LAPTO" computer.
VERBOSE: [PARAM  ] Including the "Log" parameter with the "ToScreen" value.
VERBOSE: [BEGIN  ] Entering the Begin block [Function: Get-WorkDomainComputer].
VERBOSE: [PROCESS] Entering the Process block [Function: Get-WorkDomainComputer].
VERBOSE: [END    ] Entering the End block [Function: Get-WorkDomainComputer].

Okay, Info and Param aren’t block locations like Begin, Process, and End. Even so, I’ve found them to be helpful. This value is stored in the $BL variable originally, and, as you can see, the value is changed while a function is executing. It occurred to me recently, and randomly, that someone else that builds a tool from this same function template might create their own $BL, or $bl (it doesn’t matter) variable. This would cause a mess! This whole thing would be better if the variable were protected: enter the variable read-only option. While I could’ve made it a constant, the function (my code) wouldn’t be able to change its value, but you knew that already.

So, I changed all my $BL variable assignments to these, which are a part of the template function code now.

New-Variable -Name BL -Value '[INFO   ]' -Option ReadOnly
Set-Variable -Name BL -Value '[PARAM  ]' -Force
Set-Variable -Name BL -Value '[BEGIN  ]' -Force
Set-Variable -Name BL -Value '[PROCESS]' -Force
Set-Variable -Name BL -Value '[END    ]' -Force

Now, if a function author uses my function template and without knowing they shouldn’t, tries to create a variable using $BL, they won’t break what the function template is doing in the background. PowerShell will put a stop to that before they get too far (without making the function template explode). Here’s how it appears in my testing; this should look familiar.

Cannot overwrite variable BL because it is read-only or constant.
At line:222 char:9
+         $BL = 'testing'
+         ~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (BL:String) [], SessionStateUnauthorizedAccessException
    + FullyQualifiedErrorId : VariableNotWritable

Now, I’m going to need to consider the nested functions in the function template and their protection, too. It would be less likely they’d choose the same name of those, but I’m of the mindset that we should put in protection even if we don’t think we’ll need it.

Oh, the last thing (but not really). I had this thought: Can I make a [preference variable]() read-only?

PS C:\> $VerbosePreference
SilentlyContinue
PS C:\> Set-Variable -Name VerbosePreference -Option ReadOnly
PS C:\> $VerbosePreference = 'Continue'
Cannot overwrite variable VerbosePreference because it is read-only or constant.
At line:1 char:1
+ $VerbosePreference = 'Continue'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : WriteError: (VerbosePreference:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable

PS C:\>

I can! I can see where this may be helpful for my function template, too!! I didn’t bother trying it, but this variable likely can’t be made to be a constant. This option has to be applied at the time a variable is created, unlike read-only, which can be applied to a variable after it’s already been created. Important note, which I mentioned earlier.

And finally, in case you cared to see the first, Active Directory example in this article using New-Variable, then here you go. It works like Math. You know, Order of Operations: complete what’s in the parentheses first, and then continue. If you didn’t include the parentheses, it would make Get-ADuser a string and assign it as the value of the $ADUser2 variable. Then, it would fail when it thought, you thought the New-Variable Cmdlet had an Identity parameter. Uhhh, no.

PS C:\> New-Variable -Name ADUser2 -Value (Get-ADUser -Identity tommymaynard)
PS C:\> $ADuser2.GivenName
Tommy
PS C:\>

Build-in Measure-Command


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on April 3, 2019.


I don’t know about you, but I have like five or six open tabs in my PowerShell editor at all times. Each was used to briefly prove something worked, or that it didn’t. Occasionally, these will become articles, but much more often, they just sit there for what feels like forever. I can’t seem to close some of these tabs, or even bring myself to save the sample code. If I can’t get to it now — or this week or month — what makes me think I’ll save it off and then get back to it? I probably won’t.

But today, I’m going to close a tab. This, right after I’ve written about it here. I’m not sure if it was supposed to become an article, but it’s going to regardless.

This tab’s single function is called Get-Date. It probably sounds familiar, and it should. Here’s the idea. I want a user to be able to invoke my function and return the date, just like the Get-Date cmdlet will do for them. Now, before we go any further, it’s important to understand that using Get-Date — the function — will absolutely remove any ability to use Get-Date — the cmdlet — in any one of the other 500 different ways it can be used. That’s okay. Remember, this came out of a tab with an unknown future. This isn’t fully thought out or tested, production-ready code. It’s an artist’s sketchbook or a student’s rough draft. I didn’t even have to use Get-Date. It could have been Get-ADUser or Set-Content. As you’ll see, the date portion of this function is inconsequential. It’s just what I chose when I needed something with which to test.

In addition to returning the standard Get-Date result, I also wanted to build in a way to measure the length of time it takes for my command to complete. This was the whole experiment; this was the purpose behind the tab. To do this, requires a user include the Measure switch parameter at the time of the function’s invocation. This measurement value isn’t displayed by default, but instead, it’s written to yes, a global variable, the user can inspect if they so desire. It still produces the Get-Date cmdlet’s default output, but now there’s more output waiting in the wings if it’s wanted.

In the first example, our Get-Date function executes as though we’ve run the default, Get-Date cmdlet. We have in fact. Without the Measure parameter, all we do is run the Get-Date cmdlet. When you look over the function’s code momentarily, you’ll see the fully-qualified, Get-Date command. A fully-qualified PowerShell command includes the module name. This is a requirement because we’ve named our function the same name as the cmdlet. Command precedence order indicates that functions execute before cmdlets if both commands have the same name.

In the second example, we’ll include the Measure switch parameter, and then return the value stored in the global, $Measurement variable. What I wanted to work, did. I built in a way to run the command and measure its time to complete if I want that information. As it’s just Get-Date, it doesn’t take long at all.

PS> Get-Date
Monday, April 2, 2019 10:24:06 PM
PS>
PS> Get-Date -Measure
Monday, April 2, 2019 10:24:07 PM
PS>
PS> $Measurement
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 1
Ticks : 10231
TotalDays : 1.18414351851852E-08
TotalHours : 2.84194444444444E-07
TotalMinutes : 1.70516666666667E-05
TotalSeconds : 0.0010231
TotalMilliseconds : 1.0231

Again, if this command is run with the Measure parameter, the length of time it took to complete is written to a global variable named $Measurement. As you’d expect, that variable will maintain its value until the variable name is reused for something else in the global scope, it’s overwritten by this function, it’s specifically removed, or the current PowerShell session has ended.

As can be seen in the below function, if the Measure parameter isn’t included, we run the fully-qualified, built-in Get-Date cmdlet. If it is included, we run Measure-Command against the fully-qualified, built-in Get-Date cmdlet. We use the OutVariable common parameter twice. We use it once with the Get-Date command, placing the current date and time into $CommandResult, and once with Measure-Command, placing the time to complete in our global $Measurement variable. We display $CommandResults and leave $Measurement in memory in case, the user opts to view the measurement results.

Function Get-Date {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [switch]$Measure
    )
 
    If ($Measure) {
        Measure-Command -Expression {
            Microsoft.PowerShell.Utility\Get-Date -OutVariable CommandResult
        } -OutVariable Global:Measurement | Out-Null
        $CommandResult
    } Else {
        Microsoft.PowerShell.Utility\Get-Date
    } # If-Else.
} # End Function: Get-Date.

Before we wrap up, let’s look at a slightly modified version of the Get-Date function. This has been named Get-DateWithPause and hopefully, this will help indicate that the function is taking proper measurements. In this function, we’ve added two Start-Sleep commands where we pause for two seconds. Beneath this function, we’ll rerun our previous examples and spot those expected differences.

Function Get-DateWithPause { 
    [CmdletBinding()] 
    Param ( 
        [Parameter()] 
        [switch]$Measure 
    )
 
    If ($Measure) {
        Measure-Command -Expression { 
            Start-Sleep -Seconds 2 
            Microsoft.PowerShell.Utility\Get-Date -OutVariable CommandResult
        } -OutVariable Global:Measurement | Out-Null 
        $CommandResult 
    } Else { 
        Start-Sleep -Seconds 2 
        Microsoft.PowerShell.Utility\Get-Date 
    } # If-Else. 
} # End Function: Get-Date. 
PS> Get-DateWithPause
Monday, April 2, 2019 10:33:44 PM
PS>
PS> Get-DateWithPause -Measure
Monday, April 2, 2019 10:33:50 PM
PS>
PS> $Measurement
Days : 0
Hours : 0
Minutes : 0
Seconds : 2
Milliseconds : 2
Ticks : 20020567
TotalDays : 2.31719525462963E-05
TotalHours : 0.000556126861111111
TotalMinutes : 0.0333676116666667
TotalSeconds : 2.0020567

I’ve got a few more tabs with random pieces of PowerShell code. I’ll take a look through those, too. Just maybe I can close a few more tabs by giving their content a purpose here, even if they’re barely worthy. That said, I think this turned out alright. Someday maybe, I’ll get a measurement option builtin to all the functions I author.

Edit: In republishing this post here on tommymaynard.com, I decided to update the function a small bit. This version includes a second switch parameter (MeasureAndShow) that will both measure the command’s duration and display it at the same time, too. With more time, I might have implemented this differently, but this works.

Function Get-Date {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [switch]$Measure,
		[Parameter()]
		[switch]$MeasureAndShow
    )

    If ($Measure -or $MeasureAndShow) {
        Measure-Command -Expression {
            Microsoft.PowerShell.Utility\Get-Date -OutVariable CommandResult
        } -OutVariable Global:Measurement | Out-Null
        $CommandResult
		if ($MeasureAndShow) {
			$Global:Measurement
		}
    } Else {
        Microsoft.PowerShell.Utility\Get-Date
    } # If-Else.
} # End Function: Get-Date.
[PS7.2.1][C:\] Get-Date

Sunday, February 13, 2022 8:17:48 AM

[PS7.2.1][C:\] Get-Date -Measure

Sunday, February 13, 2022 8:17:52 AM

[PS7.2.1][C:\] $Measurement

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 2
Ticks             : 25986
TotalDays         : 3.00763888888889E-08
TotalHours        : 7.21833333333333E-07
TotalMinutes      : 4.331E-05
TotalSeconds      : 0.0025986
TotalMilliseconds : 2.5986


[PS7.2.1][C:\] Get-Date -MeasureAndShow

Sunday, February 13, 2022 8:18:01 AM

Ticks             : 1781
Days              : 0
Hours             : 0
Milliseconds      : 0
Minutes           : 0
Seconds           : 0
TotalDays         : 2.06134259259259E-09
TotalHours        : 4.94722222222222E-08
TotalMilliseconds : 0.1781
TotalMinutes      : 2.96833333333333E-06
TotalSeconds      : 0.0001781

Require Use of a Cmdlet’s Prefix

I learned something new today. That’s always fun. Okay, not always, but when it’s about PowerShell, it often is. When you import a module — we’ll use the Active Directory module as an example — and use a prefix, you can still use the original cmdlet names. We’ll begin by importing the Active Directory PowerShell module to include a prefix.

PS > Import-Module -Name ActiveDirectory -Prefix Lol

With that module now imported, we’ll execute a Get-Command, command. As the Name parameter allows for wildcards, we’ll instruct it to return all the commands PowerShell knows about, at this stage in the session, that include the ADUser suffix from the ActiveDirectory module.

PS > Get-Command -Name '*ADUser' -Module ActiveDirectory

CommandType     Name                      Version    Source
-----------     ----                      -------    ------
Cmdlet          Get-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          New-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          Remove-LolADUser          1.0.0.0    ActiveDirectory
Cmdlet          Set-LolADUser             1.0.0.0    ActiveDirectory

The above command results indicates that PowerShell knows about four commands that end with the ADUser suffix. Each of those commands that it knows about, include the Lol prefix we used in the first command we issued, Import-Module. So far, everything is going as expected. Or is it?

Check this out. When we opt to not use the Module parameter and use Where-Object and the Source property instead, we have some different results. This is just getting weird, but at minimum, there’s a bit of proof here that the original cmdlets still exist, as well as the prefixed versions.

PS > Get-Command -Name '*ADUser' | Where-Object -Property Source -eq ActiveDirectory

CommandType     Name                      Version    Source
-----------     ----                      -------    ------
Cmdlet          Get-ADUser                1.0.0.0    ActiveDirectory
Cmdlet          Get-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          New-ADUser                1.0.0.0    ActiveDirectory
Cmdlet          New-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          Remove-ADUser             1.0.0.0    ActiveDirectory
Cmdlet          Remove-LolADUser          1.0.0.0    ActiveDirectory
Cmdlet          Set-ADUser                1.0.0.0    ActiveDirectory
Cmdlet          Set-LolADUser             1.0.0.0    ActiveDirectory

If we reissue the Get-Command command and indicate we only want the Get-ADUser command specifically, it shows up, too. Still, it’s odd that it wasn’t included in the first results. I think most of us would have expected to see it there, too. We’ll get back to this in a moment.

PS > Get-Command -Name Get-ADUser -Module ActiveDirectory

CommandType     Name                      Version    Source
-----------     ----                      -------    ------
Cmdlet          Get-ADUser                1.0.0.0    ActiveDirectory

About this time in my poking around, is when I started examining the imported module. The first thing I noted, due to the default output, was that the first two commands I could see, under ExportedCommands, didn’t include the noun prefix. It’s more proof the original cmdlets are still around.

PS > Get-Module -Name ActiveDirectory

ModuleType Version Name            ExportedCommands
---------- ------- ----            ----------------
Manifest   1.0.0.0 ActiveDirectory {Add-ADCentralAccessPolicyMember, Add-ADComputerServiceAcc...

The next command I ran was to better view these exported commands. I haven’t included all the results, but I’m sure you’ll get the idea.

PS > (Get-Module -Name ActiveDirectory).ExportedCommands

Key                                               Value
---                                               -----
Add-ADCentralAccessPolicyMember                   Add-LolADCentralAccessPolicyMember
Add-ADComputerServiceAccount                      Add-LolADComputerServiceAccount
Add-ADDomainControllerPasswordReplicationPolicy   Add-LolADDomainControllerPasswordReplicationPolicy
Add-ADFineGrainedPasswordPolicySubject            Add-LolADFineGrainedPasswordPolicySubject
Add-ADGroupMember                                 Add-LolADGroupMember
Add-ADPrincipalGroupMembership                    Add-LolADPrincipalGroupMembership
Add-ADResourcePropertyListMember                  Add-LolADResourcePropertyListMember
Clear-ADAccountExpiration                         Clear-LolADAccountExpiration
Clear-ADClaimTransformLink                        Clear-LolADClaimTransformLink
Disable-ADAccount                                 Disable-LolADAccount
...

You’ll notice that we’re dealing with a hash table. For every key named with the original cmdlet name, we have a value that is the cmdlet name with the noun prefix. The hash table appears that it may be used to know the relationship between the original cmdlets and its matching prefixed version. For fun, I opened a new PowerShell session and imported the Active Directory module without the prefix. In that case, the above hash table had the same command for a Key and its matching Value.

This finding made me think of something. Perhaps that Get-Command, command that included the Module parameter, only checked for commands against the values in this hash table, but the Where-Object cmdlet, returned the keys and values from the hash table. Just a thought; I said I get back to this a moment ago.

So here’s why I really decided to write. What if I don’t want to allow a user to use the original command. I don’t want them to use Get-ADUser to invoke Get-ADUser; instead, I want them to use the prefixed version only. Why, I don’t exactly know, but here’s how I fixed that one.

We’ll pretend that we’ve yet to import the Active Directory module; we’re starting fresh with the below sequence of commands. We’ll begin by creating two variables: $Module and $Prefix. In the subsequent command, we’ll import the Active Directory module providing it’s yet to be imported. When it’s imported, we’ll include the Prefix parameter with the Lol value. This means, Get-ADUser becomes Get-LolADUser, as can be figured out from the previous examples.

PS > $Module = 'ActiveDirectory'; $Prefix = 'Lol'

PS > If (-Not(Get-Module -Name $Module)) {
>>> Import-Module -Name $Module -Prefix $Prefix
>>> }

PS > $ExportedCommands = (Get-Module -Name $Module).ExportedCommands

PS > Foreach ($Command in $ExportedCommands.GetEnumerator()) {
>>> New-Item -Path "Function:\$($Command.Key)" -Value "Write-Warning -Message 'Please use the $($Command.Key.Replace('-',"-$Prefix")) function.'" -Force
>>> }

After that, we’ll get all of the ExportedCommand into a variable with the same name. Then, the Foreach command iterates over the keys in our hash table — the original cmdlet names — and creates a function for each one of them. One minute, Get-ADUser executes as expected, and the next minute, it doesn’t. Now, every time someone enters the actual AD cmdlet name, a function with the same name will execute instead. It will indicate to the user to use the prefixed version. Here’s a few examples.

PS > Get-ADUser -Identity tommymaynard
WARNING: Please use the Get-LolADUser function.

PS> Unlock-ADAccount tommymaynard
WARNING: Please use the Unlock-LolADAccount function.

PS > Get-ADDomain
WARNING: Please use the Get-LolADDomain function.

PS > (Get-LolADUser tommymaynard).SamAccountName
tommymaynard

No idea if this might ever prove useful, but at minimum, I now know I can change the default behavior of a cmdlet, in order to point someone toward the prefixed version. Oh, and the reason why the function I created is run instead of the cmdlet, is because of command precedence. Read about it; it’s beneficial to know. I knew it existed and I’ve used it, I’ve just never used it in bulk to create functions to run in place of so many cmdlets. Additionally, however, I’m not sure if I’ve ever created a function by using the New-Item cmdlet directly against the Function PSDrive. Neat.

Enjoy the weekend.

Update: Ah, it’s Monday again. This seems to happen at least once a week. There’s something I wanted to note, and it’s not just the day. Even though we’re using a function to indicate to users to use the prefixed cmdlet, the original cmdlet can still be used. If someone uses the full path to the cmdlet, the function will be ignored. That’s to say, this still works.

PS > ActiveDirectory\Get-ADUser

Handle a Parameter Whether it’s Included or Not

I’m in the process of writing a new tool. It’s basically a wrapper function for Get-ADObject that only returns Active Directory (AD) contact objects. While there’s a Get-ADComputer cmdlet to get AD computer objects, and a Get-ADUser cmdlet to get AD user objects, there’s no specific cmdlet for contacts. There’s no surprise here really, and that’s likely why we have a Get-ADObject cmdlet. It’s for those AD objects that didn’t make the cut and get their own.

I’ve seen some discussion on this in the past: How do I handle running a cmdlet within a function when I don’t know if a parameter and parameter value will be included, or not? Let’s consider that my Get-ADContact function will run the Get-ADObject cmdlet, as seen below. In the Begin block, we create a $Parameter variable that will hold a hash table. We’ll populate it in such a way that the key Filter will have a corresponding value of {ObjectClass -eq ‘contact’}. In the Process block, we splat this hash table, currently with a single parameter and parameter value, to the Get-ADObject cmdlet.

Function Get-ADContact {
    [CmdletBinding()]
    Param (
    )

    Begin {
        # Create parameter hash table; add Filter parameter.
        $Parameters = @{Filter = {ObjectClass -eq 'contact'}}
    } # End Begin.

    Process {
        Get-ADObject @Parameters
    } # End Process.

    End {
    } # End End.
} # End Function: Get-ADContact.

There’s two other parameters included in Get-ADObject that I want to allow my users the ability to include. That’s the -SearchBase and -SearchScope parameters. You can read more by checking the help for Get-ADObject: Get-Help -Name Get-ADObject. There’s actually several AD cmdlets that also include these parameters. They can be quite helpful, so that’s why I’ve decided to include them.

Before we continue, I want to let the audience know that I am familiar with creating proxy functions, and I understand it might’ve been a better option. Maybe I’ll circle back sometime and see about replicating the functionality I’ve created here, in that manner. It might turn out this wasn’t worth writing and posting. No guarantee, but it’s possible.

Okay, back on track. Let’s add the additional lines inside the Param block, that make accepting a -SearchBase and -SearchScope parameter value possible.

Function Get-ADContact {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [string]$SearchBase,

        [Parameter()]
        [ValidateSet(0,'Base',1,'OneLevel',2,'SubTree')]
        $SearchScope
    )

    Begin {
        # Create parameter hash table; add Filter parameter.
        $Parameters = @{Filter = {ObjectClass -eq 'contact'}}
    } # End Begin.

    Process {
        Get-ADObject @Parameters
    } # End Process.

    End {
    } # End End.
} # End Function: Get-ADContact.

Now, our Get-ADContact function will include the two additional parameters. Neither parameter is mandatory, but the -SearchScope parameter does include a ValidateSet parameter attribute to ensure it’ll only accept the values 0, 1, 2, Base, OneLevel, or SubTree. Base and 0 are equivalent, as are 1 and OneLevel, and 2 and SubTree.

The next thing I need to do is include the parameter values assigned to the -SearchBase and -SearchScope parameters to our $Parameters hash table when those are included. I decided to do this using the $PSBoundParameters variable, the ForEach-Object cmdlet, and the switch language construct.

Function Get-ADContact {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [string]$SearchBase,

        [Parameter()]
        [ValidateSet(0,'Base',1,'OneLevel',2,'SubTree')]
        $SearchScope
    )

    Begin {
        # Create parameter hash table; add Filter parameter.
        $Parameters = @{Filter = {ObjectClass -eq 'contact'}}

        $PSBoundParameters.Keys | ForEach-Object {
            Switch ($_) {
                'SearchBase' {$Parameters += @{SearchBase = $SearchBase}; break}
                'SearchScope' {$Parameters += @{SearchScope = $SearchScope}}
            }
        }
    } # End Begin.

    Process {
        Get-ADObject @Parameters
    } # End Process.

    End {
    } # End End.
} # End Function: Get-ADContact.

All done. Now, we have function that only returns AD contact objects. Additionally, we have the option of narrowing down our search by including the often used -SearchBase and -SearchScope parameters. While I don’t doubt there’s a better way, I think this one will work for now.

Write-Output Gets Foreground and Background Colors and More

Every once in a while a forum post comes along that really captures my interest. Take this one for instance.

In this post, the creator wanted to build out a start up menu in the ConsoleHost that has a solid white line, another white line below that, with the words “PowerShell Project 1” in it, and a final white line beneath that. That might be difficult to imagine, so here’s a visual representation, and then what his/hers looked like. I had recommended to use a here-string to fix the problem, but unfortunately we didn’t have the same results.

write-output-gets-foreground-and-background-colors-and-more01
Note: While you can’t see them, in both this and the below image, there are spaces on the lines above, below, and before and after “PowerShell Project 1.” This is how we can ensure the white rectangle around the text.

write-output-gets-foreground-and-background-colors-and-more02

I can’t say for certain why theirs rendered differently, but I suspect version, or PSReadline, or possibly console font/size. That wasn’t important; what was, was trying to figure out a way around this problematic inconsistency. We have the option to change the background color of the area where we type, and so I wondered if I could temporarily do that inside the ConsoleHost. It turns out I could. With that new piece of knowledge, I set out to write a wrapper function around Write-Output. In my version — Write-TMOutput — it includes -ForegroundColor and -BackgroundColor parameters with an option to horizontally and vertically pad the text (object).

First, here’s a few images to show my new function in action. This first image shows the commands included in the TMOutput module.

write-output-gets-foreground-and-background-colors-and-more03

The next example image shows some basic, Write-Output type usage. One where we pipe to the function, and one where we don’t. Fairly standard.

write-output-gets-foreground-and-background-colors-and-more04

Now, the next example is where this starts to get fun: the incorporation of the -ForegroundColor and -BackgroundColor parameters. Prior to running these commands, and those in the last two examples, I removed the PSReadLine module so the colors would be easier to spot.

write-output-gets-foreground-and-background-colors-and-more05

Next is a demo of two additional parameters: -HorizontalPad and -VerticalPad. These will allow you to add spaces before and after the value supplied to the -InputObject parameter (the text), and add lines above and below the text. This is the part that gets back to the request in the Microsoft Technet forum post. I should mention that my solution in the forum post would be different now that I’ve spend some time writing this function — there are better ways to do things that I hadn’t considered at the time I replied to the post.

write-output-gets-foreground-and-background-colors-and-more06

The next example shows some usage with the Get-ADUser cmdlet. Keep in mind that if you combine this function with other cmdlets, that the -HorizontalPad and -VerticalPad parameters cannot be used. In my mind, they’re just a bonus to the option of using colors with Write-Output. I should mention it now, but this function was intentionally written to only work in the ConsoleHost. Perhaps I’ll add the option of using it in the ISE in the future.

write-output-gets-foreground-and-background-colors-and-more07

I’ve written an additional function that will allow you to quickly see the available colors from which you can choose. It’s called Show-TMOutputColor, and produces the results below. This might come in handy as you’re using the modified Write-Output.

write-output-gets-foreground-and-background-colors-and-more08

Now, on to the module. It’s the first time I’ve done it, but I’ve uploaded this to the PowerShell Gallery. I believe publishing to the PowerShell Gallery was one of my PowerShell New Year’s Resolutions for 2016, so there’s one down! You can use this direct link or, one better, download the module from your console, using this command:

Install-Module -Name TMOutput

Thanks for reading this post. Hopefully these function will be a useful way to get around the need for Write-Host’s foreground and background color options while working in the console. That, and maybe the TechNet thread creator can do whatever they wanted, too.

Check for Active Directory User in Function Parameters

I’m not sure what it is, but this might’ve been the longest I’ve gone without writing. It’s not that I’ve stopping thinking PowerShell, it’s that I’m so consumed, I’ve had a hard time focusing. My wife calls it my adult ADD. It was just last week that I attended my second PowerShell + DevOps Global Summit (that’s the new name). I learned a great deal and I had a great time. Thanks again to PowerShell.org for the contest and free 4-day pass, my employer for picking up the flight, lodging, and food, my parents for their help, and of course to my wife! The weather in Bellevue, WA was perfect and the summit was a huge success. Much gratitude to all those involved in the planning and execution.

It wasn’t long ago that I had a post about comparing Active Directory group memberships. I wanted a pleasant way to visualize and compare the group memberships of two AD users. We had a ticket earlier today, at the office, where I went back to that post and gathered the commands, so I could use them again. I knew I’d need it, I just didn’t know I’d need it so soon. I sent the output results as an image to a coworker, and it wasn’t but a few seconds later that he was interested in what was used to gather that output: it was my modified compare. Here’s an edited version of the image I sent. This image differs from the original post in that I added the users’ SamAccountNames as the property names in my collection. This is why they’re blurred out this time around.

check-for-active-directory-user-in-function-parameters01

I mentioned to him that I’d write the commands into a wrapper function. While that’s nearly done, I asked myself this question: When should I check if the users exist in Active Directory? Do I do it as part of the function (inside the Begin block, for instance), or do I check at the time the parameter is accepted, as part of a ValidateScript validation?

I’m here today to say that making it a part of the ValidateScript validation attribute is acceptable. While it has the potential to cause an error condition and dump red text to the console, that’s fine. People good at Windows PowerShell read these errors — they’re PowerShell’s errors, and we’re just allowing them to occur. While I can catch these in the function, PowerShell doesn’t always do this for us natively, so I as I see it, it’s not always necessary. Knowing the audience for your tool can help you with this determination. Do you want them to learn PowerShell and get comfortable with the native errors, or does it make sense to protect them from seeing these? It’s up to you. I’m okay with allowing my tools to display some of the native errors, especially since we’re talking about my team using these tools, and not a lower-tiered group. Again, you have to consider who will use your tools, and how they’ll handle the red text.

The test function I wrote, and some error results, can been seen below. In my mind, there were two error possibilities: the computer where the function is invoked doesn’t have the ActiveDirectory module, or one, or both, of the users don’t exist in Active Directory. Again, in many cases, these standard PowerShell errors can be delivered without the need to hide them from the user of the function. Here’s a test function now.

Function Test-ADUser {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateScript({Get-ADUser -Identity $_})]
        [string]$User1,

        [Parameter(Mandatory=$true,Position=1)]
        [ValidateScript({Get-ADUser -Identity $_})]
        [string]$User2
    )

    Write-Output -InputObject "Success on both parameters."
}

This first image below, shows the error condition when the ActiveDirectory module isn’t available on the system where the function is invoked. It displays the standard, I-don’t-recognize-what-you-entered error: “Get-ADUser is not recognized…”

check-for-active-directory-user-in-function-parameters02

In this image, we can see the error message that occurs when one of the supplied users doesn’t exist in Active Directory. It won’t get this far unless the ActiveDirectory module is on the system where this function is invoked.

check-for-active-directory-user-in-function-parameters03

So in the end, consider when writing your tools, if a standard PowerShell error is acceptable or not. I think that there’s times it’s suitable, depending on the error and the group using the tool.

Only Return System.DateTime Properties from Get-ADComputer

I had one of those randomly appearing PowerShell questions last night. Windows PowerShell is a huge interest for me, so there’s really no surprise.

I wondered, How can only return the date time related properties from Get-ADComputer? It seems to happen quite often that I’ll need to view date and time information from Get-ADComputer (and Get-ADUser). It’s mildly cumbersome to scan though all the properties looking for dates and times in the property’s value — the ultimate reason behind this random thought.

The command I threw together was ugly, and seemed to lack a simpler approach. I stopped there, and decided to pass this one out to the community. Can you come with a better way?

Again, the idea is to return only the properties from Get-ADComputer that are System.DateTime properties (have System.DateTime in the Definition property of Get-Member). Take a look at my example and you might better understand my goal. I didn’t bother filtering out the default properties returned by Get-ADComputer (at first), but you’re welcome to do that, too. Cheers!

PS> Get-ADComputer -Identity SERVER01 -Properties ((Get-ADComputer -Identity SERVER01 -Properties * | Get-Member | Where-Object Definition -match 'System.DateTime').Name)

AccountExpirationDate  :
AccountLockoutTime     :
Created                : 9/12/2013 1:34:06 PM
createTimeStamp        : 9/12/2013 1:34:06 PM
DistinguishedName      : CN=SERVER01,OU=Finance,DC=mydomain,DC=com
DNSHostName            : SERVER01.mydomain.com
Enabled                : True
LastBadPasswordAttempt :
LastLogonDate          : 3/17/2014 10:35:35 AM
Modified               : 8/18/2014 11:48:34 AM
modifyTimeStamp        : 8/18/2014 11:48:34 AM
Name                   : SERVER01
ObjectClass            : computer
ObjectGUID             : 234cbaed59-1ab3-6ebc-9782-e9542bedaec
PasswordLastSet        : 3/14/2014 5:12:24 PM
SamAccountName         : SERVER01$
SID                    : S-1-5-21-174985627-956854884-123956358-942569
UserPrincipalName      :
whenChanged            : 8/18/2014 11:48:34 AM
whenCreated            : 9/12/2013 1:34:06 PM

Update1: I went ahead and edited the command so that it would not return the default Get-ADComputer properties (Name, SamAccountName, etc.), using the Select-Object cmdlet. I repeated the command I issued to the -Properties parameter of Get-ADComputer, as the value for Select-Object.

So, any takers? Can you come up with a better way to do this?

PS> Get-ADComputer -Identity SERVER01 -Properties ((Get-ADComputer -Identity SERVER01 -Properties * | Get-Member | Where-Object Definition -match 'System.DateTime').Name) | Select-Object (Get-ADComputer -Identity SERVER01 -Properties * | Get-Member | Where-Object Definition -match 'System.DateTime').Name

AccountExpirationDate  :
AccountLockoutTime     :
Created                : 9/12/2013 1:34:06 PM
createTimeStamp        : 9/12/2013 1:34:06 PM
LastBadPasswordAttempt :
LastLogonDate          : 3/17/2014 10:35:35 AM
Modified               : 8/18/2014 11:48:34 AM
modifyTimeStamp        : 8/18/2014 11:48:34 AM
PasswordLastSet        : 3/14/2014 5:12:24 PM
whenChanged            : 8/18/2014 11:48:34 AM
whenCreated            : 9/12/2013 1:34:06 PM

Update2: There’s a second update to this post. Jonathan Angliss tweeted a much cleaner solution — exactly what I had hoped someone might do. Here’s his contribution:

PS> Get-ADComputer -Identity SERVER01 -Properties * | ForEach-Object {$_.psobject.properties | Where-Object {$_.TypeNameofValue -eq 'System.DateTime'}} | Select-Object Name,Value

Invoke a Command from a Get-History Menu

Sometimes you line up the right cmdlets, appreciate what you’ve done for a moment, only to become mildly irritated that you never thought of that before. I just did that.

We all know, or should rather, that the Get-History cmdlet returns a list of commands that have been entered during the current Windows PowerShell session. If you open up a new console, enter a couple commands, and then run Get-History, or one of its aliases (ghy, h, history), it’ll show you what commands you’ve entered up until that point. Take this example, for instance:

PS> Get-Process | Select-Object -Last 2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    263      11     1680       5132    46     0.02   3356 WUDFHost
    227      12     1824       6804    47     0.22   3840 WUDFHost


PS> Get-Service | Select-Object -First 2

Status   Name               DisplayName
------   ----               -----------
Running  AdobeARMservice    Adobe Acrobat Update Service
Stopped  AdobeFlashPlaye... Adobe Flash Player Update Service


PS> Get-History

  Id CommandLine
  -- -----------
   1 Get-Process | Select-Object -Last 2
   2 Get-Service | Select-Object -First 2


PS> Get-History

  Id CommandLine
  -- -----------
   1 Get-Process | Select-Object -Last 2
   2 Get-Service | Select-Object -First 2
   3 Get-History

I use the up arrow quite often to cycle through my previous commands, and press Enter when I’ve found the one I want to run again. That’s one way to rerun a previously run command. Another, is to use the Get-History cmdlet. First, you use it to determine the ID of the command you want to rerun, such as we’ve done above. Then, you can run Get-History again with the -Id parameter and pipe that result to Invoke-History (aliases: ihy, r), such as we’ve done below. You can also just use Invoke-History -Id 1 without the use of Get-History.

PS> Get-History -Id 1 | Invoke-History
Get-Process | Select-Object -Last 2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    263      11     1680       5132    46     0.02   3356 WUDFHost
    227      12     1824       6804    47     0.22   3840 WUDFHost

What about making a “menu,” where we can select the command we want to run and press OK to run it? Easy. If you’ve been paying attention to PowerShell, then you’ve probably seen the Out-GridView cmdlet before. Let’s put it to good use, and I’ll show you the cmdlets I lined up.

PS> Get-History | Out-GridView -PassThru | Invoke-History

When I enter the command above, it will show the dialog box below; however, your dialog box will be the default size. I’ve resized mine so that the image better fits on this page. If it’s important to you, you can change what the title says by including Out-GridView’s -Title parameter: Get-History | Out-GridView -PassThru -Title ‘History Menu’ | Invoke-History.

Invoke-a-Command-from-Get-History-Menu01

After I select an option from the list, and press OK, the history item will be rerun.

PS> Get-History | Out-GridView -PassThru | Invoke-History
Get-Process | Select-Object -Last 2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    263      11     1680       5132    46     0.02   3356 WUDFHost
    227      12     1824       6804    47     0.22   3840 WUDFHost

PS>

Well, there it is. Something so simple and so obvious that I can’t believe I never thought of it before.

Report on Active Directory Objects in Abandoned Organizational Unit

Before we really start this post, I should mentioned that there’s no reason that the script discussed in this post can’t be run against an Organizational Unit (OU) that hasn’t been abandoned. It just worked out that I wrote the script in order to determine if an OU had be abandoned.

I threw a small script together in the last couple days and thought I’d share it. The reason for the script was because I may have had an Active Directory (AD) OU that was no longer being used. In order to determine if this was really the case, I wanted to check various properties on the user and computer objects in the OU, to include any nested OUs. These properties included the last logon time stamp, the last date the objects changed, and a few others.

The first couple of lines in the script set two different variables. The first one stores the Domain’s Distinguished Name, and the second one is assigned the location of the abandoned OU. The second variable is based partly on the first. This script requires the ActiveDirectory module and assumes it’s being run on PowerShell 3.0 or greater, as the AD module isn’t explicitly imported.

$DomainDN = ((Get-ADDomain).DistinguishedName)
$AbandonedOU = Get-ADObject -Filter * -SearchBase "OU=Finance,OU=Departments,$DomainDN"

In the next part of the script, we start to send the $AbandonedOU variable’s objects across the pipeline, to the Foreach-Object cmdlet. As each object passes across, we determine what type of AD object we’re dealing with. If it’s a user object, we set the $Command variable to the string, Get-ADUser. If it’s a computer object we set the $Command variable to the string, Get-ADComputer. If it’s neither, such as a nested OU, we’ll return to the $AbandonedOU variable and send the next object without assigning anything to the $Command variable (or running any of the upcoming code).

$AbandonedOU | ForEach-Object {
    If ($_.ObjectClass -eq 'user') {
        $Command = 'Get-ADUser'
    } ElseIf ($_.ObjectClass -eq 'computer') {
        $Command = 'Get-ADComputer'
    } Else {
        return
    }

Providing we have a user or computer AD object, we’ll run the code in the next example. This will execute the cmdlet, whether it be Get-ADUser or Get-ADComputer, returning the requested properties that we then calculate (think, customize).

    & $Command -Identity $_ -Properties * |
        Select-Object Name,
            @{N='Type';E={$_.ObjectClass}},
            @{N='Created';E={$_.whenCreated}},
            @{N='Last Logon TimeStamp';E={[datetime]::FromFileTime($_.LastLogonTimeStamp)}},
            @{N='Changed';E={$_.whenChanged}},
            @{N='Added To Domain By';E={$_.nTSecurityDescriptor.Owner}}
}

Finally, we sort the collection of objects we’ve returned and customized, and in my case, pump the data out to a CSV file at the root of my C:\ drive. As you’ll see below, I’ve included both the code in the previous example and the additional code.

    & $Command -Identity $_ -Properties * |
        Select-Object Name,
            @{N='Type';E={$_.ObjectClass}},
            @{N='Created';E={$_.whenCreated}},
            @{N='Last Logon TimeStamp';E={[datetime]::FromFileTime($_.LastLogonTimeStamp)}},
            @{N='Changed';E={$_.whenChanged}},
            @{N='Added To Domain By';E={$_.nTSecurityDescriptor.Owner}}
} | Sort-Object 'Last Logon TimeStamp' -Descending | Export-Csv -Path C:\AbandonedOU.csv -NoTypeInformation

I want to mention something about the line above that calculates the “Added To Domain By” property. In many environments this is going to only be <DOMAIN>\Domain Admins. The reason I added this, is because in the AD environment in which this ran, users, other than the Domain Admins, can join computers. I know this is a default; however, in many environments it is not allowed. This may or may not be a helpful property in your environment.

Cheers, and thanks for reading! I’ve included the complete script below.

$DomainDN = ((Get-ADDomain).DistinguishedName)
$AbandonedOU = Get-ADObject -Filter * -SearchBase "OU=Finance,OU=Departments,$DomainDN"

$AbandonedOU | ForEach-Object {
    If ($_.ObjectClass -eq 'user') {
        $Command = 'Get-ADUser'
    } ElseIf ($_.ObjectClass -eq 'computer') {
        $Command = 'Get-ADComputer'
    } Else {
        return
    }

    & $Command -Identity $_ -Properties * |
        Select-Object Name,
            @{N='Type';E={$_.ObjectClass}},
            @{N='Created';E={$_.whenCreated}},
            @{N='Last Logon TimeStamp';E={[datetime]::FromFileTime($_.LastLogonTimeStamp)}},
            @{N='Changed';E={$_.whenChanged}},
            @{N='Added To Domain By';E={$_.nTSecurityDescriptor.Owner}}
} | Sort-Object 'Last Logon TimeStamp' -Descending | Export-Csv -Path C:\AbandonedOU.csv -NoTypeInformation