Instructions:
The requirements don’t tell:
- what operating system will be used to run the code
- what version of powershell will be used to execute the code
- whether the Active Directory module is present and loaded
- what version of Windows Domain Controllers are running
- how the Active Directory is configured: replication,…
I made 3 assumptions for this event:
- We won’t use PowerShell V1 because Get-Random cmdlet isn’t available. Writing a piece of code to simulate the behavior of Get-Random would have been interesting but was totally out-of-scope for this event. Anyway this doesn’t mean that we cannot come up with clever ways of selecting 20 pseudo randomly users.
- If the domain admin that will run the code has installed Active Directory modules for Powershell (using RSAT) and still has only Active Directory running on Windows 2003, he has also taken care of installing Active Directory Management Gateway Service. The domain admin will not run the code on a broken plateform.
- The instructions say “A Domain Admin will always run the command”. It doesn’t tell if he will run the code from a computer that doesn’t belong to Active Directory. I assumed he will select a machine (client or server) that belongs to an Active Directory because that’s just common sense. Checking that the computer is a member of a domain and not a workgroup isn’t a requirement.
One of the most difficult tasks in this event is that there are many ways to query Active Directory, some techniques return the raw value and some display a nicely formatted value. They don’t have the same properties name and count returned by default.
The Get-ADUser cmdlet returns by default 10 properties. Using core cmdlets like Get-Item on a user object while navigating the AD drive will return only 4 properties by default.
Get-ADUser will display a nicely formatted property PasswordLastSet that is derived from the 64bit integer value named pwdLastSet…
To make it even more difficult, you should find out how accurrate these properties are, whether they are replicated across all domain controllers, how active directory stores them and how some of these properties are calculated. One of the most common mistake is to use the LastLogon property. This property isn’t replicated across DCs, it’s thus unreliable. The lastlogontimestamp is replicated but it’s not 100% accurate.
To illustrate that and to get more information about Active Directory, I’ve got some really nice links to share from the following sources:
- The Hey Scripting Guy‘s blog
- How Do I Search Active Directory? (2009-03-16)
- How Can I Use Windows PowerShell to Search Active Directory? (2010-02-01)
- Querying Active Directory via PowerShell (2012-08-13)
- Use PowerShell to Search AD DS and Produce an Uptime Report (2012-08-13)
- The Ins and Outs of Using DSQuery with Windows PowerShell (2012-08-14)
- Avoid Loading the AD: Drive with the Active Directory Module (2013-03-18)
- Find Active Directory User Info with the PowerShell Provider (2013-03-19)
- Use PowerShell to Find Non-Default User Properties in AD (2013-03-20)
- Ashley McGlone
- Everything you need to get started with Active Directory (2012-01-03)
- Five free ways to script Active Directory in PowerShell: Part 2 (2012-03-14)
- Free Download: CMD to PowerShell Guide for AD(2013-01-02)
- Ask the Directory Services Team
- The LastLogonTimeStamp Attribute – What it was designed for and how it works (2009-04-15)
(2010-07-17)- How do I find out what changes are going on in my Active Directory? (2010-12-06)
(2012-01-21)
(2012-02-05 )- The http://www.powershellmagazine.com PSTip
- List all AD attributes currently in use for AD users (2013-05-06)
- List all Active Directory constructed attributes (2013-04-05)
- Extending the default output property set for Active Directory objects (2013-04-30)
- The Microsoft support
- Wiki pages from MVP Richard Mueller
- Active Directory: PowerShell AD Module Properties
- Active Directory: Get-ADUser Default and Extended Properties
(you can click on images, it will redirect you to the AskDS team’s post)
How to use the UserAccountControl flags to manipulate user account properties
(a must read, if you haven’t yet
)
- Testing whether the Active Directory module is present or not
To be absolutely sure that the orginal module from Microsoft is present and being used, I first remove any module named “ActiveDirectory”.
Remove-Module -Name "ActiveDirectory" -Force -ErrorAction Stop
Then I try to load the original module from Microsoft. As I don’t know if the $env:PSModulePath has been modified, I try to load the module from its original path. Yes, the Import-Module cmdlet allows filepath as input.
Get-Item "$env:systemroot\system32\WindowsPowerShell\v1.0\Modules\ActiveDirectory" -EA Stop | Import-Module -ErrorAction Stop
For fun, I decided to use the core cmdlets (Get-ChildItem, Get-Item,…) to query the AD: drive. I needed to know whether the drive was mounted along with the module import or disabled on purpose using the $Env:ADPS_LoadDefaultDrive variable:
if (Test-Path Env:ADPS_LoadDefaultDrive) { if ([bool]($Env:ADPS_LoadDefaultDrive)) { # The drive is loaded because the value is not 0 $ADDriveLoaded = $true Write-Verbose "AD drive mounted by default using environment variable" } else { # $Env:ADPS_LoadDefaultDrive = 0 -> don't load Write-Verbose "AD drive mounting was disabled using environment variable" if ($Fun) { try { # Mount the drive using a Global Catalog New-PSDrive -PSProvider ActiveDirectory -Name AD -Root "" -server ( Get-ADDomainController -Discover -Service 2 -ErrorAction Stop ).Name -ErrorAction Stop $ADDriveLoaded = $true Write-Verbose "AD drive mounted in current scope" } catch { $ADDriveLoaded = $false Write-Warning -Message "Failed to mount AD drive in current scope because $($_.Exception.Message)" } } else { $ADDriveLoaded = $false } } } else { # The AD drive is loaded by default $ADDriveLoaded = $true Write-Verbose "AD drive mounted by default" }
I have noticed during the testing phase, that the Active Directory provider no longer allow a Recursive search in Powershell 3.0 as it did in Powershell 2.0
I had to add a mandatory path to an OU to workaround that issue.
if ($Fun -and $ADDriveLoaded) { Write-Verbose "Using the Active Directory module in fun mode" Switch ($PSVersionTable.PSVersion.Major) { 2 { $GCIHT = @{ Path = "AD:\$Path" Recurse = $true } break } default { $GCIHT = @{Path = "AD:\$Path"} } } $allusers = @(Get-ChildItem -Filter "(&(objectClass=user)(objectCategory=person))" @GCIHT)
I could not validate the path of the OU in the parameter block as I needed to know whether the AD drive was mounted or not. I added the following in the Begin block:
# Validate the OU if ($Fun -and $ADDriveLoaded) { if (-not(Test-Path -Path "AD:\$($Path)" -PathType Container)) { Write-Warning "Invalid OU path provided" break } }
Can the userAccountControl attribute be used to determine if the user account is locked or if his password is expired?
The KB article kb305144 says that:

Here’s also a comment on the MSDN page about the userAccountControl

Thanks to the Powershell magazine PSTip I have seen that the msDS-User-Account-Control-Computed AD attribute is a constructed attribute
Thanks to Richard Mueller and his wiki page we also know how these attributes are calculated:








As you can see, this wiki page is just awsome, it actually answers many questions
When using ADSI or the AD: drive, I’ve used this kind of code to display nicely formatted properties:
1..$MaxUsers | ForEach-Object { $i = (Get-Random -Maximum $MaxRandom -Minimum 0) $propsar= 'sAMAccountName','department','title','userAccountControl','lastLogonTimestamp', 'pwdLastSet','msDS-User-Account-Control-Computed' $user = Get-Item -Path "AD:\$($allusers[$i].distinguishedName)" -Properties $propsar New-Object -TypeName PSObject -Property @{ UserName = $user.sAMAccountName Enabled = -not($user.userAccountControl -band 2) Department = $user.department Title = $user.title LastLogonDate = switch ($user.lastLogonTimestamp) { $null { 'Never' ; break } default { [datetime]::FromFileTime([int64]::Parse($_)) } } LockedOut = [bool]($user.'msDS-User-Account-Control-Computed' -band 16) PasswordExpired = [bool]($user.'msDS-User-Account-Control-Computed'-band 8388608) PasswordLastSet = switch ($user.pwdLastSet) { 0 { if ($user.userAccountControl -band 65536) { 0 ; break } else { 'Must Change password at next logon' ; break } } default { [datetime]::FromFileTime([int64]::Parse($_)) } } } } | Select -Property UserName,Enabled,Department,Title,LastLogonDate,LockedOut,PasswordExpired,PasswordLastSet | Sort -Descending -Property PasswordLastSet
Having the PwdLastSet attribute set to 0 doesn’t necessarily means that the user must change his password at next logon
(source)
Performance wasn’t a requirement. But I didn’t know how many user objects are present in Active Directory. We could have 1000, 10 000 or even more. To get results more quickly, I added a range to the MaxUsers parameter and used its upper limit as a value for the ResultSetSize parameter of the Get-ADUser cmdlet.
[parameter()] [ValidateRange(0,1000)] [int]$MaxUsers=20
$allusers = ActiveDirectory\Get-ADUser -Filter * -ResultSetSize 1000
ADSI has also a way to set the maximum number of objects returned.
$Searcher = [System.DirectoryServices.DirectorySearcher]$Root $Searcher.SizeLimit = 1000
Unfortunately, we cannot instruct core cmdlets to get a certain number of items. As I discovered the issue with the recursive search and made the OU path a mandatory parameter, I could get 0 or more AD users objects. Yes, I could get 0 object if you type a valid OU path where there isn’t any user object. Worse, I noticed another problem when there’s only 1 user object. My Get-Random cmdlet was throwing an error because you can’t do:
Get-Random -Maximum 1 -Minimum 1

In the previous event, I used the awsome work of Don Jones who wrote a free ebook named “Creating HTML Reports in PowerShell” that you can download on this page: http://powershell.org/wp/newsletter/
The instruction said that:
Get PowerShell to do as much of that validation as possible – Dr.Scripto has to review your code, and doesn’t want to read a lot of manual validation
script.
I spent some time testing and reading the help about ValidateScript.
I think I found a way to put as much as validation as possible in the ValidateScript attribute, keep it easy to read, grasp the logic and of course maintain.
The only thing I wanted was to make the error behave as if it was thrown by a cmdlet.
I wanted to avoid the full ValidateScript content to be displayed in the console when an error occurs because having too many lines could be somehow “counter-productive”. I’m also not a big fan of using the throw keyword… but sometimes you just need it
[parameter()] [ValidateScript({ # Test that the parent path exists if (-not(Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container)) { throw New-Object System.ArgumentException -ArgumentList "Filepath doesn't exist" } # Validate the filename extension if ( (Split-Path -Path $_ -Leaf) -notmatch ".*\.html?$") { throw New-Object System.ArgumentException -ArgumentList "File extension must end by .html or .htm" } return $true })] [string]$FilePath = "$Env:TEMP\audit.report.html"
And here’s the result

Not bad? What do you think?
Final word
This event was really hard but also fun. It was a good opportunity to learn many many things about Active Directory, like ADSI,…
I’m not super proud of how my entry looks like but I know it does the job.
You can have a look at it: http://scriptinggames.org/entrylist_.php?entryid=914
If you see anything that’s wrong or that I can improve, I welcome your feedback. If you find something you don’t understand, leave a comment on this blog post, I’ll try to explain it.







. The more WMI classes you query, the longer it will last.


















