Tag Archives: Pester

Pester to validate ADUser properties

‘Sup PSHomies,

See if you can relate. You’re in the middle of a migration, the users need to be created asap. You get a xlsx file with all the necessary properties. A quick Copy/Paste to csv file, Import-Csv  user.csv -Delimiter "`t" | New-ADUser and presto! Whew! Glad we got that out of the way 😉

Feels pretty awesome right? 15 minutes after, your project manager comes asking: “Say, which file did you use?” The one you sent me last week, why? “Uh, there’s a new version on sharepoint, did you use that one?” Well I did ask which file I should use (in my defense I did, that’s why I always email, written proof!). “Well there’s an updated version, could you make sure the users get updated? Thanks!!!” Sigh, here we go again…

At this point I can do two things:

  1. Just delete and recreate. Thing is you’ll loose SIDs and access to homedirectory etc etc. Not exactly ideal.
  2. Update the user properties. Definitely a better option. Still tricky especially using the Set-ADUser cmdlet, but that’s another story.

But before you go off to update the user settings, how about validating what has been changed? Maybe the damage isn’t that bad. I mean if it’s under five changes, I just might  do it manually… Oh who am I kidding? Wait, gimme a minute to catch my breathe from laughing! 😛

Enter Pester for ADUser validation!

With a Pester script to validate your ADUser settings, you’ll never have to second guess if the settings are as they should.

<#
Author: I.C.A. Strachan
Version: 1.0
Version History:
Purpose: Pester script to validate ADUser properties.
#>
[CmdletBinding()]
Param(
[string]
$csvFile = 'Users.csv',
[Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]
$Encoding = 'UTF8'
)
$csvParam = @{
Path = ".\source\csv\$csvFile"
Delimiter = "`t"
Encoding = $Encoding
}
$csvADUsers = Import-Csv @csvParam
$ADPropertiesToVerify = ($csvADUsers | Get-Member | Where-Object {$_.membertype -eq 'noteproperty'}).name
Foreach ($user in $csvADUsers){
#Get AD User attirbutes
try{
$verify = Get-ADUser Identity $user.SamAccountName Properties *
if ($verify) {
Describe "AD User operational readiness for $($user.DisplayName)" {
Context 'Verifying ADUser Attributes'{
ForEach($attribute in $ADPropertiesToVerify){
if (([string]::isNullOrEmpty($user.$attribute))) {
$user.$attribute = $null
}
if($attribute -eq 'Path'){
it "User is located in $($user.$attribute)" {
$verify.DistinguishedName.Contains($user.$attribute)
}
}
else{
it "User property $($attribute) value is $($verify.$attribute)" {
$user.$attribute | Should be $verify.$attribute
}
}
}
}
Context 'Verifying ADUser HomeDirectory Security'{
it 'User HomeDirectory attribute is not empty'{
$user.HomeDirectory | Should not be $null
}
It "Homedirectory $($user.HomeDirectory) exists"{
Test-Path $user.HomeDirectory | Should be $true
}
It "User is owner of $($user.HomeDirectory)"{
(Get-Acl $user.HomeDirectory).Owner| Should be "$($env:USERDOMAIN)\$($user.sAMAccountName)"
}
}
}
}
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]{
Write-Error Message "User $($user.SamAccountName) account NOT present"
}
catch {
Write-Error Message "Unhandled exception looking up $($user.SamAccountName)) account."
throw $_
}
}

Here’s the result:ADPesterResults

Here’s a quick rundown of the script:

First I’ll just get all the user settings using $verify = Get-ADUser -Identity $user.SamAccountName -Properties *.

$ADPropertiesToVerify =  ($csvADUsers | Get-Member | Where-Object {$_.membertype -eq 'noteproperty'}).name will get me all the properties in the csv file. No need to map properties manually. Now I can loop through any amount of properties!

Next up, making sure empty properties get $null

if (([string]::isNullOrEmpty($user.$attribute))) {
   $user.$attribute = $null
}

$null isn’t equal to empty (Ofcourse you already knew that!)

Now compare what’s in the csv to what Get-ADUser found:

if($attribute -eq 'Path'){
   it &quot;User is located in $($user.$attribute)&quot; {
      $verify.DistinguishedName.Contains($user.$attribute)
   }
}
else{
   it &quot;User property $($attribute) value is $($verify.$attribute)&quot; {
      $user.$attribute | Should be $verify.$attribute
   }
}

Quick note: I used Path to create the user in a specific OU. There’s no Path property in Get-ADUser. So I did the next best thing, just verify that path is part of the user’s distinguishedname 😉

I also added a little bonus to verify the user’s homedirectory exists and that the user is also the owner.

Being able to validate will definitely give you peace of mind…

Hope it’s worth something to you

Ttyl,

Urv

Pester script to validate GPOs Scope of Management

So here’s another spin on using Pester to validate operational readiness… 😉

Group policies can be pretty tricky! Troubleshooting can be a challenge. There might be even times that you start doubting yourself. Depending on the link order of your Policies, you might not get what you expected…

Operations is dynamic, things get moved around, enabled/disabled, blocked, name it and it’s bound to happen.

How about… some way to validate your GPOs Scope of Management! Once everything is working as it should, create a validation set you can verify later on. Trust me, I’ve been there… Using Pester will definitely give you that edge…

So I improvised a little on Ashley’s McGlone’s GPO Report and made a function Get-GPOsSoM. Just be sure to save it in the same folder as Domain-GPOSoM.Tests.ps1

<#
Author: I.C.A. Strachan
Version: 1.0
Version History: Based on Ashley McGlone's Get-GPOReport. Here's a shortlink to it: http://tinyurl.com/ofpfnf4
Purpose: Get all GPOs that are linked to Domain, Sites and/or OUs
#>
Function Get-GPOsSoM {
BEGIN{
Import-Module GroupPolicy Verbose:$false
Import-Module ActiveDirectory Verbose:$false
#region Get a list of all GPOs
$GPOs = Get-GPO All |
Select-Object ID, Path, DisplayName, GPOStatus, WMIFilter
#endregion
#Array for GPLinks results
$gPLinks = @()
#region GPO Linked to the Domain
$domainGPO = @{
Identity = ((Get-ADDomain).distinguishedName)
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPlinks += Get-ADObject @domainGPO |
Select-Object 'name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname',
@{name='Depth';expression={0}}
#endregion
#region GPO Linked to OUs
$ouGPOs = @{
Filter = '*'
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPLinks += Get-ADOrganizationalUnit @ouGPOs |
Select-Object name, distinguishedName, gPLink, gPOptions ,canonicalname ,
@{name='Depth';expression={($_.distinguishedName -split 'OU=').count 1}}
#endregion
#region GPOs linked to sites
$siteGPOs = @{
LDAPFilter = '(objectClass=site)'
SearchBase = "CN=Sites,$((Get-ADRootDSE).configurationNamingContext)"
SearchScope = 'Onelevel'
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPLinks += Get-ADObject @siteGPOs |
Select-Object name, distinguishedName, gPLink, gPOptions ,canonicalname,
@{name='Depth';expression={0}}
#Hashtable to lookup GPOs
$lookupGPO = $GPOs | Group-Object AsHashTable Property 'Path'
}
PROCESS{
#Get the Scope of Management of each gPLink
ForEach ($SOM in $gPLinks) {
if ($SOM.gPLink) {
If ($SOM.gPLink.length -gt 1) {
$links = @($SOM.gPLink -split {$_ -eq '[' -or $_ -eq ']'} | Where-Object {$_})
For ( $i = $links.count 1 ; $i -ge 0 ; $i ) {
$GPOData = $links[$i] -split {$_ -eq '/' -or $_ -eq ';'}
[PSCustomObject]@{
Depth = $SOM.Depth;
Name = $SOM.Name;
DistinguishedName = $SOM.distinguishedName;
canonicalName = $SOM.canonicalname;
PolicyDN = $GPOData[2];
LinkOrderNr = $links.count $i
GUID = $lookupGPO.$($GPOData[2]).ID;
DisplayName = $lookupGPO.$($GPOData[2]).DisplayName;
GPOStatus = $lookupGPO.$($GPOData[2]).GPOStatus;
WMIFilter = $lookupGPO.$($GPOData[2]).WMIFilter.Name;
Config = $GPOData[3];
LinkEnabled = [bool](!([int]$GPOData[3] -band 1));
Enforced = [bool]([int]$GPOData[3] -band 2);
BlockInheritance = [bool]($SOM.gPOptions -band 1)
}
}
}
}
}
}
END{}
}

view raw
Get-GPOsSoM.ps1
hosted with ❤ by GitHub

Now for the fun part! 🙂

<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Pester script to validate Group Polcies status and Link on Domain,Sites and OUs
#>
[CmdletBinding()]
Param()
Describe 'Group Policies Scope of Management validation' {
BeforeAll {
#region Get GPOs Producution Validation set
$gpoValidationSet = @'
DisplayName,DistinguishedName,GPOStatus,BlockInheritance,LinkEnabled,Enforced,LinkOrderNr
Default Domain Policy,"DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,1
Default Domain Controllers Policy,"OU=Domain Controllers,DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,1
WinRM Listeners,"OU=Servers,DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,1
RemoteDesktop,"OU=Servers,DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,2
Firewall,"OU=Servers,DC=pshirwin,DC=local",UserSettingsDisabled,FALSE,TRUE,TRUE,3
'@ | ConvertFrom-Csv Delimiter ','
#endregion
#Dot source Function Get-GPOsSOM.ps1
. $PSScriptRoot\Get-GPOsSOM.ps1
#Create hashtable for lookup
$lookupGPOInReport = Get-GPOsSOM | Group-Object AsHashTable Property 'DisplayName'
}
It 'GPOs Scope of Managment has been retrieved' {
$lookupGPOInReport | should not BeNullOrEmpty
}
It 'GPO validation set has been retrieved' {
$gpoValidationSet | Should not BeNullOrEmpty
}
foreach($set in $gpoValidationSet){
Context "GPO: $($set.DisplayName)" {
it "GPO $($set.DisplayName) exists" {
$lookupGPOInReport.$($set.DisplayName) | Should Not BeNullOrEmpty
}
it "GPO is linked to $($set.DistinguishedName)"{
$lookupGPOInReport.$($set.DisplayName).DistinguishedName | Should be $set.DistinguishedName
}
it "BlockInheritance: $($set.BlockInheritance)" {
$lookupGPOInReport.$($set.DisplayName).BlockInheritance | Should be $set.BlockInheritance
}
it "LinkEnabled: $($set.LinkEnabled)" {
$lookupGPOInReport.$($set.DisplayName).LinkEnabled | Should be $set.LinkEnabled
}
it "Group policy Enforced: $($set.Enforced)" {
$lookupGPOInReport.$($set.DisplayName).Enforced | Should be $set.Enforced
}
it "Group policy LinkOrder nr: $($set.LinkOrderNr)" {
$lookupGPOInReport.$($set.DisplayName).LinkOrderNr | Should be $set.LinkOrderNr
}
it "Group policy status: $($set.GPOStatus)" {
$lookupGPOInReport.$($set.DisplayName).GPOStatus | Should be $set.GPOStatus
}
}
}
}

So here’s the result:

Pester Test GPO SoM

Now Imagine someone changed your GPO link order:

Pester Test GPO Change Link Order

Run Pester test script again:

Pester Test GPO Change Link Order -Detected

No more doubt! The link order has been tampered with! This is definitely a game changer for Operations!

My new motto : “If you can automate it, you should test it” 😛

Pester for everyone!

Hope it’s worth something to you

Ttyl,

Urv

Pester script to Test DNS Configuration

So I recently blogged about Configuring DNS zones and resource records. While going through my twitter feed, I stumbled upon this little gem by Kevin Marquette. He recently did a session on Pester and uploaded his demo. If you’re interested in Pester (as you should) you should definitely check it out!

So one of the demos was a eureka moment for me: The Active.Directory.System.DC.tests.ps1!

Wait you can do that? I thought Pester was about unit Framework testing not about validating script output. So I can test if my script did what I expected it to do? (Pause to let that sink in). Well alrighty then!!! 😛

So I decided to give it a go for the DNS Configuration.

<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Pester script to validate that DNS Zones and Records have been configured
#>
[CmdletBinding()]
Param(
[string]$fqdn ='domain.local',
[string[]]$ServerIPAddress= @('192.168.1.4', '192.168.1.5')
)
Import-Module DNSServer Verbose:$false
Describe "DNS Exchange Configuration Test for $fqdn" {
$zoneNames = @(
"autodiscover.$($fqdn)"
"mail.$($fqdn)"
"webmail.$($fqdn)"
"_autodiscover._tcp.$($fqdn)"
)
Context 'DNS Zones' {
# Test every zone
foreach($zoneName in $zoneNames){
it "Has a zone named: $zoneName" {
Get-DnsServerZone $zoneName | Should Not BeNullOrEmpty
}
}
}
Context 'DNS Resource records' {
foreach ($IPAddress in $ServerIPAddress){
foreach($zoneName in $zoneNames){
It "Has IPAddress $($IPAddress) in zone $($zoneName)"{
(Get-DnsServerResourceRecord ZoneName $zoneName | out-string) | Should Match $IPAddress
}
}
}
}
}

And here’s a screenshot of the results:

Pester-DNS

So there’s more to Pester that meets the eye… Imagine the possibilities… No wonder Microsoft is shipping Pester with 2016…

Hope it’s worth something to you

Ttyl,

Urv