I’m bit behind the schedule with this event. That happens: life sometimes interferes plans.
Also, today I wanted to focus on two parts of assignment: 20 random users, translating Active Directory date into nice (DateTime) format.
How to pick random users from Active Directory? First idea: use Get-Random on all users found. But is that a good idea? I assume most scripters tested against “test” domain, or didn’t care about performance (in my production domain search for all users took 3 minutes, so it’s not real issue). But I wanted to give it more thought. We already played with HTML reporting, and most of advanced scripters used parameter validation from day one (so part of the task that requires file extension validation was something they would do anyways). So today: few ideas on how one can grab this X random (X for advanced category, where it suppose to be parameterized). BTW: I decided to do it without any snappins/ modules – pure PowerShell and ADSI/ ADSISearcher. Behold!
Simplest is best – usually…
At first I just cheated. It gives some result that are random, but the pool that we take all users from is not full AD, just part of it that will fit in one Page. If you use ADSISearcher and skip PageSize parameter, you won’t get all results (on my box it’s 2000). But that’s pretty fast – and in the end we can just pull 20 out of 2000, and it makes results relatively random. On my box it took around 30 seconds to get it. Function that I’ve used:
function Get-ADXUser { param ( $Count = 20 ) ([ADSISearcher]@{ Filter = '(&(ObjectClass=user)(ObjectCategory=person))' SearchRoot = [ADSI]'' }).FindAll() | Get-Random -Count $Count }
What if I want to do it for real and grab random users from whole domain? Adding PageSize should do, but the performance impact of it is significant. My AD has ~ 21k user objects. Using this function:
function Get-ADRandomUser { param ( $Count = 20 ) ([ADSISearcher]@{ Filter = '(&(ObjectClass=user)(ObjectCategory=person))' SearchRoot = [ADSI]'' PageSize = 1000 }).FindAll() | Get-Random -Count $Count }
It took 5 minutes to complete. Not the end of the world, but really makes you think if 2000 isn’t enough as a pool for picking random user.
Getting carried away
Next idea seemed smart at first. It wasn’t. So why do I mention it in the first place? Well, I thought it might be interesting as mind-muscle exercise. My idea:
- pick unique attribute that is possible to generate randomly
- look for user object with that value
- repeat until I get expected count of user objects
I expected this to be relatively fast. It wasn’t I tried few times and it was always around 7 minutes. Picking field was easiest part: it could be only SID. But what SIDs can I use? As you know SID has two parts: domain SID and RID. RID max value can be found on domain object. I just grabbed domain SID and max RID, and used syntax that lets you connect directly to object that has given SID in your domain. Time it took depends on how lucky you are with selecting RIDs, best result was still bigger than with function that looks for all users and just picks 20 at random. So this is just brain exercise. Whole function:
function Get-ADRandomSid { param ( $Count = 20 ) $MyAd = [ADSI]'' $MaxRid = (New-Object ADSISearcher -ArgumentList @( [ADSI]"LDAP://CN=Rid Manager$,CN=System,$( $MyAd.distinguishedName )" ) ).FindOne().Properties.ridavailablepool | Foreach-Object { $_ % [int64][math]::Pow(2,32) } $DomainSid = ( New-Object Security.Principal.SecurityIdentifier -ArgumentList @( , $MyAd.objectSid[0] 0 ) ).Value $List = New-Object Collections.ArrayList while ($List.Count -lt $Count) { $Sid = "{0}-{1}" -f $DomainSid, (1..$MaxRid | Get-Random) [ADSI]"LDAP://<SID=$Sid>" | Where-Object { $_.ObjectClass -eq 'user' -and $_.ObjectCategory -match '^CN=Person,CN=Schema,CN=Configuration' } | ForEach-Object { $List.Add($_) | Out-Null } } $List }
I’ve used different sources to come up with the logic, modifying most of them a little bit. I guess logic around picking random RID could be improved, but still – with requirement of picking random user objects (not just any objects) I suspect all where-object tests are bottle neck here. If I just pick 20 objects at random, it takes 3 minutes. Still – no thrill there.
Cheating… differently
Finally I decided to try different approach. What if I would just take a round with few filters, grab only number of objects I need from each filter and combine results to form my “pool”. This pool would be used than to grab final “chosen ones”. Take a look:
function Get-ADRandomEmployeeId { param ( $Count = 20 ) 1..9 | % { ([ADSISearcher]@{ SizeLimit = $Count Filter = "(&(objectClass=user)(objectCategory=person)(employeeId=$_*))" }).FindAll() } | Get-Random -Count $Count } function Get-ADRandomLogin { param ( $Count = 20 ) 97..122 | % { ([ADSISearcher]@{ SizeLimit = $Count Filter = "(&(objectClass=user)(objectCategory=person)(samaccountname=$([char]$_)*))" }).FindAll() } | Get-Random -Count $Count }
First one completed in 30 seconds, second – in 14 seconds. Results look convincingly random. Functions are very easy. Obviously, you have to try fields that are present for all users so either fields that are required (like sAMAccountName) or something that is always populated (and preferably – unique) in your organization (employeeId in mine). And that’s what I eventually decided to use – sort-of random, quick way to pick random 20 users.
Issues with dates
Active Directory dates are stored as large integer that is equal to number of 100 nanoseconds intervals since January 1, 1600 (UTC). Not really readable format, if you would ask me… There are few ways to translate this value to “normal” DateTime. But probably none of those is as simple as using DateTime static method – FromFileTime.
That would work fine for pwdLastSet attribute. But we are required to give auditor information about last login time, and that’s probably biggest challenge – luckily, there are many examples online how to solve it. The reason for that issue is the fact that lastLogon attribute is not replicated between DCs, so if we query DC – we will get information for that DC only. If user authenticates to different DC our result will be wrong. Two possible solutions: assume lastLogonTimeStamp (replicated) is close enough, or query all DCs and pick largest value. So in the end – whole code that I would use to get information for given user:
(New-Object ADSISearcher -ArgumentList @( '(sAMAccountName=Locked)' , @( 'pwdLastSet' 'samaccountname' 'department' 'title' 'lastlogontimestamp' 'useraccountcontrol' 'lockouttime' ) )).FindOne().Properties | ForEach-Object { [PsCustomObject]@{ Name = -join $_.samaccountname PwdLastSet = [datetime]::FromFileTime($_.pwdlastset[0]) Department = -join $_.department Title = -join $_.title LogedOn = [datetime]::FromFileTime($_.lastlogontimestamp[0]) Disabled = [bool]($_.useraccountcontrol[0] -band 2) Locked = ($_.lockouttime[0] -gt 0) } }
Obviously, instead of filter used above – I would use one of the filters discussed. Once I would get object – I could convert it to HTML and eventually, get report for auditor. I didn’t (time and few events prevented me from getting script done) – but I hope you picked up few tricks anyways. And now to judging – I’m behind with that too…
I did something very similar for the AD filter! Grabbing all the users and storing them in a variable just seems wasteful. I would love some feedback if you have time sir 🙂
http://scriptinggames.org/entrylist_.php?entryid=913