Tag Archives: OperationValidation

Taking ADUser validation a step further…

β€˜Sup PSHomies,

This is me every single time I sit down to use Pester…

PesterOVF.PNG

Hehe…

While I do enjoy using Pester for operational validation, what do you do with the ones that fail? Most of the time you’re the one doing the validation in the chain of process and everything goes right! Because you’re that good… πŸ˜‰ Hehe…Β  I’ve been recently asked to update some users where some attributes didn’t get populated during a migration… Are you thinking what I’m thinking? πŸ˜‰

The first thing I did was export the attributes they needed, easy enough. Next was to change the necessary attributes to the correct value. The ones that differ will fail (Of course they will Urv, not a total retard here… get on with it… )

Here’s where having proper descriptions can go a long way.

Breadcrumbing

This is what a Test ErrorRecord looks like when you capture the results:PesterError

At first I thought of using regex on the ErrorRecord. Some of theΒ  attributes aren’t set which gave me some issues, so I decided to breadcrumb the name. First do a split on the colon ‘:’ grab the last part and do a split again using ‘/’ to get the attribute name and value. Don’t forget to trim() πŸ™‚

There were some other Attributes that weren’t part of the default parameter set of the Set-ADUser cmdlet. To change those attributes you need to use the DisplayName and the Replace Operator. For the attributes “not set” that need to be cleared, use the Clear operator. Just don’t use both the parameter and the DisplayName! I had EmailAddress and mail in the CSV file, one passed and the other failed… I got rid of mail…

Ok here’s the code to get things done:

First get failed Pester tests.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Validate AD User attributes
#>
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
$csvFile = 'd:\scripts\ps1\source\csv\SetUser.csv'
)
#region Import Csv file
$csvUsers = Import-Csv -Path $csvFile -Delimiter "`t" -Encoding UTF8
$userProperties = ($csvUsers | Get-Member -MemberType NoteProperty).Name |
Where-Object {$_ -ne 'OU'}
#endregion
#region Main
$csvUsers |
Foreach-Object {
$Expected = $_
Describe "Processing User: $($Expected.SamAccountName)" {
Context "Verifying AD User properties for $($Expected.DisplayName)" {
#Get AD user properties
$Actual = Get-ADUser -Identity $Expected.SamAccountName -Properties $userProperties
ForEach( $property in $userProperties){
if (([string]::isNullOrEmpty($Expected.$property))) {
$Expected.$property = $null
$lableExpected = '<not set>'
}
else{
$lableExpected = $Expected.$property
}
it "Verifying user property: $($property) / $($lableExpected)"{
$Actual.$property | Should be $Expected.$property
}
}
}
}
}
#endregion
#Run PesterTest and save results
$resultsTest = Invoke-Pester D:\scripts\ps1\dsa\ADUser.Properties.Tests.ps1 -PassThru
#Get All failed tests
$failedTests = $resultsTest.TestResult.where{$_.Passed -eq $false}
$failedTests |
ForEach-Object{
$result = $_.Name.Split(':')[-1]
$arrResult = $result.Split('/')
[PSCustomObject]@{
SamAccountName = ($_.Describe.split(':').Trim())[-1]
Property = $arrResult[0].Trim()
Expected = $arrResult[1].Trim()
}
} -OutVariable failedObjects
#Export Failed objects
$failedObjects |
Export-Csv -Path D:\Scripts\ps1\source\csv\FailedTests.csv -Delimiter "`t" -NoTypeInformation -Encoding UTF8

This will give me a csv file with the following Columns, SamAccountName, Property & Expected (Value).

The set failed Pester tests will either set or clear the attribute depending on it’s value. If it’s $null it will be cleared.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Set ADUser attributes of failed tests
#>
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
$csvFile = 'D:\Scripts\ps1\source\csv\FailedTests.csv'
)
$FailedTests = Import-Csv -Path $csvFile -Delimiter "`t" -Encoding UTF8
#Get Set-ADUser Parameters
$setADUserParameters = (Get-Command Set-ADUser).ParameterSets.Parameters.Where{ $_.IsDynamic -eq $true} | Select-Object -ExpandProperty Name
#Get User Property
$FailedTests |
Foreach-object{
#Set Expected to null if <not set>
$Expected = @{$true = $null; $false = $_.Expected}[$_.Expected -eq '<not set>']
If ($setADUserParameters.Contains($_.Property)){
$paramSetADUser = @{
Identity = $_.SamAccountName
$_.Property = $Expected
}
Set-ADUser @paramSetADUser
}
else{
if($Expected){
Set-ADUser -Identity $($_.SamAccountName) -Replace @{$_.Property = $Expected}
}
else{
Set-ADUser -Identity $($_.SamAccountName) -Clear $($_.Property)
}
}
}

Here are some screenshots to give you an idea what to expect.

Ideally you’d only have a few failed tests. I wouldn’t use this to reset entire User attributes. Moving and/or renaming an object isn’t supported… yet! πŸ˜‰

So there you have it, taking failed Pester tests values and setting them accordingly!

Hope it’s worth something to you,

Ttyl,

Urv

 

 

Verify GroupMembership with Pester

β€˜Sup PSHomies,

Here’s another advantage of addingΒ members with a different approach, Pester validation!

This makes for an easy way to process validation of Β each member of a group.

Quick update: I’ve added Β some extra code (at the end of the previous blog code)Β toΒ export added- and revoked members.

#region Export for futher processing
$GroupMembers =@{
  Groups  = $Header
  Added   = $addADGroupMembers
  Revoked = $delADGroupMembers
}
$GroupMembers |
Export-Clixml .\export\dsa\ADGroupMembers-$exportDate.xml -Encoding UTF8
#endregion

Quick rundown, first we’ll import the saved object and used that to get a snapshot of the current group members. Then it’s time to vaildate who has been added or revoked.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Validate group membership being added or revoked
#>
[CmdletBinding()]
param(
$xmlFile = 'ADGroupMembers-12102016.xml'
)
#Get saved group members from xmlFile
$SavedGroupMembers = Import-Clixml .\export\dsa\$xmlFile
#region Get current Group memberships
$SnapshotADGroupMembers = @{}
$SavedGroupMembers.Groups |
ForEach-Object{
$SnapshotADGroupMembers.$($_.SamAccountName) = Get-ADGroupMember -Identity $_.SamAccountName| Select-Object -ExpandProperty SamAccountName
}
#endregion
#region Verify members being revoked.
$SavedGroupMembers.Revoked.Keys |
ForEach-Object{
$GroupName = $_
if($SnapshotADGroupMembers.$GroupName){
Describe "AD GroupMembership revoked operational readiness for $GroupName" -Tags Revoked{
Context "Verifying users whose membership has been revoked from $GroupName."{
$SavedGroupMembers.Revoked.$GroupName |
ForEach-Object{
It "User $($_) is not a member of $($GroupName)"{
!($SnapshotADGroupMembers.$GroupName.Contains($_)) | Should be $true
}
}
}
}
}
}
#endregion
#region Verify members being added.
$SavedGroupMembers.Added.Keys |
ForEach-Object{
$GroupName = $_
if($SnapshotADGroupMembers.$GroupName){
Describe "AD GroupMembership added operational readiness for $GroupName" -Tags Added{
Context "Verifying users who are members of $GroupName."{
$SavedGroupMembers.Added.$GroupName |
ForEach-Object{
It "User $($_) is a member of $($GroupName)"{
($SnapshotADGroupMembers.$GroupName.Contains($_)) | Should be $true
}
}
}
}
}
}
#endregion
#region Save Current membership for future reference
$SnapshotADGroupMembers |
Export-Clixml .\export\dsa\SnapshotADGroupMembers-$exportDate.xml -Encoding UTF8
#endregion

adgroupmemberresults

In this case I wanted to generate different Describe blocks. This makes for a better distributionΒ in the HTML report.

adgroupmembernunithtml

Here’s the code to generate the HTML report using reportunit.exe

#region
$exportDate = Get-Date -Format ddMMyyyy
#endregion

#region Main
$pesterGroupMembers = Invoke-Pester .\ps1\dsa\ADGroupMembers*  -OutputFile .\export\dsa\ADGroupMembers.NUnit.xml -OutputFormat NUnitXml -PassThru

#run reportunit against ADgroupMembers.NUnit.xml and display result in browser
&amp; .\tools\ReportUnit\reportunit.exe .\export\dsa\ADGroupMembers.NUnit.xml
Invoke-Item .\export\dsa\ADGroupMembers.NUnit.html

#Export Pester results to xml
$pesterGroupMembers | Export-Clixml .\export\dsa\PesterResults-GroupMembers-$($exportDate).xml -Encoding UTF8
#endregion

Making sure a user is a member can be tricky at times especially when the members list is a few hundred.

As always, snapshots are your friend! When I exported the groups the first time I did it without validating if they existed. I recently ran into a situation where AD Objects were being deleted and recreated using the same SamAccountName! So having a little more information than just the SamAccountName can help when troubleshooting now and in the future.

When my project manager asked for logs and I handed him the HTML generated report of the group members… You should have seen the glee on his face!

So there you have it, verfying group membership using Pester!

Hope it’s worth something to you…

Ttyl,

Urv

Operational Readiness validation gotchas

Sup’ PSHomies,

Back from the PSConfEu 2016 in Hannover! It was awesome!!! It was great meeting so many in person! I highly recommend attending a conference if ever given the chance! Tobias did a great job organizing PSConfEU 2016!

The presentations were top notch! Two presentations I definitely wanted to follow were June Blenders’ Real world Pester TDD tests & Ravikanth Chaganti Operations Validation Framework. I enjoyed Ravikanth’s approach: a simplistic and a comprehensive test. June’s presentationΒ was insightful! There were definitely a few aha moments for me! SoΒ I decided to re-evaluate what I had learned so far now that I’ve seen how it should be done! πŸ˜‰ .

Simplistic tests

Think of simplistic testsΒ as kicking the tires. Here’s where the obvious tests go:

  • Are the sets aligned?
  • Did the object count meet your expectation?

Things of that nature. Here are some tests to help you understand some gotchaΒ moments.

Validating the count seems pretty straight forward right? Well, not always… I’ll explain…

When the sets to validate are identical validation is pretty straight forward. This is the best case scenario. I did one validation with and without Group-Object (I’ll explain later on).

Simplistic Identical sets - Count

Simplistic Indentical sets

By using Group-Object I can get the “real” count of a set. Group-Object will gauge the uniqueness of the set, but I also found a test where that might not always be useful… So my next test was to omit an entry from the Verify set

Simplistic Missing entry sets - Count

Simplistic missing an entry in Verify

The test failed as it should. Now for some fun, let’s add a double entry to the Verify set.

 

Simplistic Double entry sets - CountThis is an odd test, it could be a typo. Imagine having a list where double entries aren’t that obvious, this should catch it. Now ideally you’d use your source code that you used during deployment. I’m assuming you did automate your process eh? πŸ˜‰ Without the Group-Object the count is identical. With Group-Object you only have one entry! This could explain an exception happened during deployment… Accidents will happen…

The last test is a fun one: different sets.

Simplistic Different sets - Count

Different sets

Surprise! Both validation count test passed, but the sets are totally different!

Bonus Test!

Simplistic Different sets double entry - Count

Different set with a double entry

This one almost got away. While doing the comprehensive test, it dawned on me that I should be testing both counts, with and without Group-Object. I updated the screenshot accordingly.

Take away Simplistic test:

Don’t only rely theΒ count of a set. By using Group-Object you can gauge a set’s uniqueness. The bonus test showed that exceptions may happen. It’s totally valid as a starting point, that’s why it’s aΒ simplistic tests. Here’s the code for the Simplistic test for count validation:

$savedADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

$verifyADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

#region Example Operational validation Simplistic test
Describe 'Active Directory configuration operational readiness' {
   Context 'Verifying GlobalCatalogs count without Group-Object'{
      it 'Total GlobalCatalogs match' {
         @($savedADConfig.GlobalCatalogs).Count |
         Should be @($verifyADConfig.GlobalCatalogs).Count
      }
   }

   Context 'Verifying GlobalCatalogs count with Group-Object'{
      it 'Total GlobalCatalogs match' {
         @($savedADConfig.GlobalCatalogs  | Group-Object).Count |
         Should be @($verifyADConfig.GlobalCatalogs | Group-Object).Count
      }
   }
}
#endregion

If you’d like to try out the simplistic tests just add/remove entries to the saved-/verifyADConfig sets. Now for the Comprehensive tests!

Comprehensive tests

Here’s where in-depth analysis goes. When I did the AD Operational Readiness test, I had a feeling I was missing something. I saw June using sort-object in one of her validations. That triggered me to re-valuate this test.

Comprehensive Identical sets

Comprehensive Identical sets

Depending on which set you used for your enumeration you could end up with different results. When the sets are identical, all goes well. Next test, omit an entry in Verify set.

Comprehensive Missing an entry

Comprehensive missing an entry

Enumerating from the saved test caught the missing entry, enumerating from verify didn’t. Both found ‘DC-DSC-01.pshirwin.local’. The simplistic test caught this, that is why you need both! Next up: Double entry in Verify.

Comprehensive Double entry

Comprehensive double entry in verify

Enumerating from the saved set caught the double entry. Enumerating from the verify set just enumerated the entry twice. If you’re visually inclined, you might miss this.

Hey all my tests results are green and purple! Yeah…

Last test: Different sets.

Comprehensive Different sets

Different sets

At this point you’re comparing apple with oranges. This should fail.

Take away comprehensive tests:

The set you’re enumerating from matters! To cover validation, best bet is to do both! Here’s the code for the Comprehensive Test:

#region Example Operational validation Comprehensive test
$savedADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

$verifyADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

Describe 'Active Directory configuration operational readiness' {
   Context 'Verifying GlobalCatalogs enumerating from saved configuration'{
      $savedADConfig.GlobalCatalogs |
      ForEach-Object{
         it "Server $($_) is a GlobalCatalog"{
            $verifyADConfig.GlobalCatalogs.Contains($_) |
            Should be $true
         }
      }
   }
   Context 'Verifying GlobalCatalogs enumerating from verify configuration'{
      $verifyADConfig.GlobalCatalogs |
      ForEach-Object{
         it "Server $($_) is a GlobalCatalog"{
            $savedADConfig.GlobalCatalogs.Contains($_) |
            Should be $true
         }
      }
   }
}
#endregion

Validation is great, but you need to make sure your validating with the right set in the right order!

Summary

  • Create Simplistic & Comprehensive tests.
  • Simplistic tests should take care of the obvious.
  • Comprehensive tests is where in-depth analysis takes place.
  • Validate by enumerating from both sets!
  • Never trust a test that doesn’t fail πŸ˜‰

I’m glad I visited both presentations! Now it’s time to update my Operational readiness tests accordingly! πŸ™‚

Hope it’s worth something to you

Ttyl,

Urv

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