PowerShell – less code, same result

One thing I have seen a lot of since I’ve been using PowerShell is that you can get the same tasks completed with less code.

Now with a bunch of the ADSI scripts we’ve gone through lately this isn’t blatantly obvious. These have been conversions of existing VBScripts that I have in my toolkit. However, this reduction in code has been highlighted really well in a recent script I converted.

When I create a new user account in Active Directory, I prefer to use a random password generator. Not particularly practical for creating one new account, but when you create say a hundred it becomes quite useful.

Ok, so here’s our first function:

—————————————
‘ Function fnRandomPassword
‘ Abstract Function to randomly generate a complex password
‘ Requirements None
‘ Parameters Integer Length of the password required.
‘ Return values random complex password to specified length
‘ Revision 1.2
‘—————————————
Public Function fnRandomPassword(imLength)

Dim smPassword,imRandValue, x
Dim imSecPoint(2), imMark
imMark = CInt(imLength/3)
‘ To meet complex password requirements we need at least 1 lower, 1 numb and 1 upper.
imSecPoint(0) = imMark
imSecPoint(1) = imSecPoint(0) + imMark
imSecPoint(2) = (imSecPoint(1) + imMark) -1
If imSecPoint(2) > imLength Then imSecPoint(2) = imSecPoint(2) -1
smPassword = “”
Randomize
Do while len(smPassword) < imLength
If Len(smPassword) = imSecPoint(0) Then
' Make sure this character is number
imRandValue = Int((128 * Rnd) + 1)
if imRandValue >= 48 and imRandValue <= 57 Then
smPassword = smPassword & Chr(imRandValue)
end if
ElseIf Len(smPassword) = imSecPoint(1) Then
' Make sure this character is an Lower case Alpha
imRandValue = Int((128 * Rnd) + 1)
if imRandValue >= 97 and imRandValue <= 122 Then
smPassword = smPassword & Chr(imRandValue)
end if
ElseIf Len(smPassword) = imSecPoint(2) Then
' Make sure this character is an Upper case Alpha
imRandValue = Int((128 * Rnd) + 1)
if imRandValue >= 65 and imRandValue <= 90 Then
smPassword = smPassword & Chr(imRandValue)
end if
Else
' Make the bugger complex :-)
imRandValue = Int((128 * Rnd) + 1)
' Due to using DCPromo.exe and HTML, we have to avoid: &<"\
if ((imRandValue => 35) And (imRandValue =< 37)) Or _
((imRandValue => 39) And (imRandValue <= 59)) Or _
((imRandValue >= 61) and (imRandValue <= 91)) Or _
((imRandValue >= 93) and (imRandValue <= 123)) then
smPassword = smPassword & Chr(imRandValue)
end if
End If
Loop
fnRandomPassword = smPassword

End Function

If you’ve played with the Randomize() Statement before then there’s nothing particularly difficult here.

We define three markers in our password where we’ll ensure that we meet complex password requirements. This falls over if the password is less then five characters, but as I usually use a minimum of eight it’s not a show stopper!

Most of the hard work in this function is testing that our random number falls within acceptable values. Then we use the Char function to produce the relevant character. If you haven’t played with Char before then Microsoft have a useful table here.

This is performed for each character up until the password length and then our resulting password is returned.

So now let’s compare our PowerShell equivalent:

function create-complexpassword
{
# *** Unable to generate complex password less then 5 chars ***
# ASCII data taken from http://msdn2.microsoft.com/en-us/library/60ecse8t(VS.80).aspx

Param (
[int]$PassLength
)

# Let’s work out where our 3 complex characters will be inserted in the password…
[int]$mark = ($PassLength/3)
$ComplexChar = @(“marker”)
$ComplexChar[0] = $mark
$ComplexChar = $ComplexChar+($ComplexChar[0] + $mark)
$ComplexChar = $ComplexChar+(($ComplexChar[1] + $mark) -1)

$Password = $null
$rnd = new-object random

# “i” is our counter while we make the password, one char at a time.
$i = $Password.length
do
{
switch ($Password.length)
{
$ComplexChar[0]
{
# Make this character a Numeric
$password = $password+([char]($rnd.next(48,57)))

}

$ComplexChar[1]
{
# Make this character a LowerAlpha
$password = $password+([char]($rnd.next(65,90)))
}

$ComplexChar[2]
{
# Make this character a Upper Alpha
$password = $password+([char]($rnd.next(97,122)))
}

default
{
# In case this is used in a DCPromo answer files, theres a few chars to
# avoid: Ampersand, Less than, double quote and back slash
$NextChar = $rnd.next(33,123)

switch ($nextChar)
{
34 {exit}
38 {exit}
60 {exit}
92 {exit}
default
{
$Password = $Password+([char]$nextChar)

}
}
}
}
$i++

}
Until ($Password.length -eq $PassLength)
return $Password

Apart from the code spacing within this function you can see that this is dramatically shorter than the VBScript counterpart. Now I guess this isn’t really a true PowerShell code reduction because we haven’t used a Cmdlet to reduce the code, this has been done using the .Net class Random and the Next method which allows us to specify some boundaries on our random number.

Personally, I think this still counts. What do you think? Do you have any good examples you’d like to share?

Searching Active Directory for Windows 2000 and Windows NT4 Domain Controllers with PowerShell

When I posted the article on raising AD functionality levels recently David Saxon pointed out that more could be done in regards to confirming the Domain Controllers are at the required level, i.e. they are Windows 2003 Domain Controllers.

Since we’ve looked at searching the Directory yesterday, this seems like a good time to take a closer look into this.
Microsoft have a good article here on what features are available depending on you functionality level.

So we’d start with:

$query = new-object system.directoryservices.directorysearcher
$root = [adsi]“”

And presuming our DC’s are located in the default location, we can narrow our search down:

$ou = [adsi](“LDAP://ou=Domain Controllers,”+$root.distinguishedName)
$query.SearchRoot = $ou
$query.SearchScope = “OneLevel”

The key to our search is going to be the filter() that we use. If you look around there’s a few different ways of identifying what we might want to search on.

In KB article 322692 there is a good filter for finding NT4 Domain Controllers:

$query.filter = “(&(objectCategory=computer)(operatingSystem Version=4*)(userAccountControl:1.2.840.113556.1.4.803:=8192))”

Here we’re filtering on the computer object, the OS version, and the UAC value. Let’s break this down a bit more. The first one is pretty obvious, the OS version is easy enough too. Anything with a 4 in the version relates to NT4. Windows 2000 is 5.0, and Windows 2003 is 5.2. So what’s with the UAC filter? The bitwise filter is an Object ID (OID) that means all the flags have to be set for a match. The value of 8192 specifies that the account is a Domain Controller.

We can search for Windows 2000 Domain Controllers using the following filter, as described in KB article 325379

$query.filter = “(&(objectCategory=computer)(primaryGroupID=516)(operatingSystemVersion=5.0*))”

Similar to the above example, we’re filtering on the computer object again. This time rather than use the UAC approach though we’ve used the PrimaryGroupID which we’ve touched on previously. The OS version corresponds to the versions discussed above.

It seems to me that our best approach would be to filter on any computer account, that’s not Windows 2003. As we’re only searching in the Domain Controllers OU we shouldn’t need to try and check the UAC or Primary Group information, but you could just to be sure if you felt that way inclined.

$query.filter = “(&(objectCategory=computer)(!operatingSystem Version=5.2*)(userAccountControl:1.2.840.113556.1.4.803:=8192))”

There’s a subtle difference between this filter and the first example. Basically we’re looking for OS version that’s not (!) equal to Windows 2003. In theory this should give us all the other Domain Controllers ;)

So to finish off then:

$result = $query.FindAll()
if ($result -eq $null)
{
return $null
}
else
{
foreach ($machine in $result)
{
$ADobject = $machine.GetDirectoryEntry()
write-host $ADobject.distinguishedName
}
}

Of course just because we have accounts in AD doesn’t actually mean that there are machines still out there. You could tie this into a function that then calls a ping test against the names that are retrieved. That’s still not gospel, but if it’s a DC, it’s more likely to be up if it’s still out there!

If you’re new to LDAP queries and would like more information Microsoft have a good primer here.