Monthly Archives: November 2015

Custom Intellisense for AD cmdlets with SearchBase parameter

Sup’ PSHomies!

You gotta love the PowerShell community! Found this little gem in my twitter feed (again) :-). Trevor Sullivan demonstrates how we can create custom intellisense for cmdlets if they haven’t been provided as yet. Great video! Trevor really does a great job explaining this.

The first thing that came to mind was Active Directory! I can’t tell you how often I needed the DistinguishedName of an OU. Now imagine having a dynamic list generated for you! No more errors,  just select and you’re good to go! Excited??? I sure am!

Sometimes you need to limit your searchbase depending on you AD size. Let’s say I want to retrieve all users starting from a specific point

Get-ADUser -Filter * -SearchBase 'OU=Users,OU=IT,DC=pshirwin,DC=local'

A simple typo will generate an error. Distinguished names are notorious for being lengthy…

Now the obvious AD cmdlets would be Get-ADUser,Get-ADGroup & Get-ADComputer. So that got me thinking , just how many AD cmdlets have SearchBase as a parameter?

Get-Command -Module ActiveDirectory |
ForEach-Object{
   $psItem.Name |
   Where-Object {
        (Get-Command $psItem).ParameterSets.Parameters.Name -eq 'SearchBase'
   }
}

Turns out there are quite a few using SearchBase

  • Get-ADComputer
  • Get-ADFineGrainedPasswordPolicy
  • Get-ADGroup
  • Get-ADObject
  • Get-ADOptionalFeature
  • Get-ADOrganizationalUnit
  • Get-ADServiceAccount
  • Get-ADUser
  • Search-ADAccount

So I can have Intellisense on all these cmdlets? Awesome!!!

<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Custom Intellisense completion for AD cmdlets with SearchBase parameter
ActiveDirectory & TabExpansion++ module is required.
Link to Trevor Sullivan's video demonstration: https://goo.gl/0TdWuv
#>
#region Get AD cmdlets with SearchBase parameter
$ADCmdlestWithSearchBase = Get-Command Module ActiveDirectory |
ForEach-Object{
$psItem.Name |
Where-Object {
(Get-Command $psItem).ParameterSets.Parameters.Name -eq 'SearchBase'
}
}
#endregion
#region Configure custom intellisense for AD cmdlets with SearchBase
$sbADSearchBase= {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$ADPaths = ActiveDirectory\Get-ADOrganizationalUnit filter *
foreach ($ADPath in $ADPaths){
$completionResult =@{
CompletionText = $ADPath.DistinguishedName
ToolTip = ('The organization unit DistinguishedName {0}' -f $ADPath.DistinguishedName)
ListItemText = $ADPath.Name
CompletionResultType = 'ParameterValue'
}
New-CompletionResult @completionResult
}
}
$tabExpansion = @{
CommandName = $ADCmdlestWithSearchBase
ParameterName = 'SearchBase'
ScriptBlock = $sbADSearchBase
}
TabExpansion++\Register-ArgumentCompleter @tabExpansion
#endregion

Intellisense completed the DistinguisedName on -SearchBase for me. No need to type it in, no errors, just select and go!

TabExpansionSearchBase

Here’s the result:

TabExpansionSearchBase-result

I’m sure you guys will find your own use for this… Thanks again Trevor for bring this to our attention! Good looking out for the community! Be sure to watch Trevor’s video for in depth explanation.

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

Configuring DNS zones and resource records

“Hey Irwin! Do you have a script to configure DNS autodiscover?” Eh, no not really… I’m assuming you’re doing something with PowerShell DNS cmdlets? 😉 And so begins another PowerShell journey…

My colleague Ofir is our Exchange guy.

“Ok so what exactly do you want to accomplish Ofir? I’m trying to automate registering some Resource records in DNS, but it isn’t working. I’d like to add some IPAddresses to a specific zone… “Ok let’s see what you’ve got!

Add-DnsServerResourceRecordA -Name "autodiscover" -ZoneName "domain.local" -AllowUpdateAny -IPv4Address "IP address 1","IP address 2","IP address 3" -TimeToLive 01:00:00

“So when I use the cmdlet directly it works. When I use varaiables it doesn’t… Ideally the code should be re-usable…”  Ofir’s words not mine… I’ll admit, I teared up a bit… Kids, they grow up so fast… Hehe…

I think we can do this…

So Ofir was using read-host to get ZoneName and IPvAddress. Ah! What a lovely opportunity to demonstrate params!

“Ok Ofir, instead of using read-host, we’re better off using parameters. Using [CmdletBinding()] gives you the possibility to use Write-Verbose, no extra charge!”

[CmdletBinding()]
Param(
   [string]$fqdn ='domain.local',
   [string[]]$ServerIPAddress
)

Now because the Resource record could be multi-valued we’ll go on and define a string array variable [string[]]$ServerIPAddress

“To make you’re code more readable we’ll just go ahead and create a hashtable we can use to splat your parameters.”

$dnsSplat = @{
   Name = 'AutoDiscover'
   Zonename = $fqdn
   AllowUpdateAny =  $true
   IPv4Address = $ServerIPAddress
   TimeToLive = '01:00:00'
}

“Now all we need to do it run the appropriate cmdlet and we’re good!”

Add-DNSServerResourceRecordA @dnsSplat

Ok, so this got Ofir started… Mind you there’s no error handling or anything of that sort…
We did some tinkering on the fly and this was the end result:

<#

Author: I.C.A. Strachan
Version:
Version History:

Purpose: Create AutoDiscover Zone and add ResourceRecord

#>
[CmdletBinding()]
Param(
   [string]$fqdn ='domain.local',
   [string[]]$ServerIPAddress= @('192.168.1.4', '192.168.1.5')
)

BEGIN{
    $dnsRRA = @{
       Name = 'AutoDiscover'
       Zonename = "autodiscover.$($fqdn)"
       AllowUpdateAny =  $true
       TimeToLive = '01:00:00'
    }

    $dnsPZ = @{
        Name = "autodiscover.$($fqdn)"
        ReplicationScope = 'Forest'
        DynamicUpdate = 'Secure'
    }

    Import-Module DNSServer -Verbose:$false
}

PROCESS{
    #Only Add Zone is count is zero (doesn't exists)
    If (@(Get-DnsServerZone $dnsPZ.name -ErrorAction SilentlyContinue ).Count -eq 0 ){
        Write-Verbose "Creating DNS Zone: $($dnsPZ.name)"
        Add-DnsServerPrimaryZone @dnsPZ
    }

    #Get string equivalent of all A records
    $RR = Get-DnsServerResourceRecord -ZoneName autodiscover.domain.local -RRType A |
    Out-String

    $ServerIPAddress | ForEach-Object {
        If (!$RR.Contains($_)){
            Write-Verbose "Adding resource record $_ to $($dnsPZ.name)"
            Add-DNSServerResourceRecordA @dnsRRA -IPv4Address $_
        }
    }
}

END{}

Ofir was quite happy! Nice! Another satisfied customer. So the other day asked him to send me the code for future reference…

This is what he sent me:

<#
Author: I.C.A. Strachan / O. Doron
Version: 1.0
Version History:
Purpose: Create AutoDiscover Zone and add ResourceRecord
Replace your local domain name and Mailbox server(s) IP address(es) with the
ones in the example:
Example:
.\Set-EX16DNSRecords.ps1 -fqdn domain.local -ServerIPAddress 192.168.1.4, 192.168.1.5 -verbose
#>
[CmdletBinding()]
Param(
[string]$fqdn ='domain.local',
[string[]]$ServerIPAddress= @('192.168.1.4', '192.168.1.5')
)
BEGIN{
$dnsRRA = @{
Name = '.'
Zonename = "autodiscover.$($fqdn)"
AllowUpdateAny = $true
TimeToLive = '01:00:00'
}
$mailRRA = @{
Name = '.'
Zonename = "mail.$($fqdn)"
AllowUpdateAny = $true
TimeToLive = '01:00:00'
}
$webmailRRA = @{
Name = '.'
Zonename = "webmail.$($fqdn)"
AllowUpdateAny = $true
TimeToLive = '01:00:00'
}
$srvRRA = @{
Name = '.'
Zonename = "_autodiscover._tcp.$($fqdn)"
DomainName = "autodiscover.$($fqdn)"
priority = '0'
weight = '0'
port = '443'
AllowUpdateAny = $true
TimeToLive = '01:00:00'
}
$dnsPZ = @{
Name = "autodiscover.$($fqdn)"
ReplicationScope = 'Forest'
DynamicUpdate = 'Secure'
}
$mailPZ = @{
Name = "mail.$($fqdn)"
ReplicationScope = 'Forest'
DynamicUpdate = 'Secure'
}
$webmailPZ = @{
Name = "webmail.$($fqdn)"
ReplicationScope = 'Forest'
DynamicUpdate = 'Secure'
}
$srvPZ = @{
Name = "_autodiscover._tcp.$($fqdn)"
ReplicationScope = 'Forest'
DynamicUpdate = 'Secure'
}
Import-Module DNSServer Verbose:$false
}
PROCESS{
#Only Add Zone is count is zero (doesn't exists)
If (@(Get-DnsServerZone $dnsRRA.zonename ErrorAction SilentlyContinue ).Count -eq 0 ){
Write-Verbose "Creating DNS Zone: $($dnsPZ.name)"
Add-DnsServerPrimaryZone @dnsPZ
}
Else{
Write-Verbose "DNS Zone $($dnsPZ.name) already exists"
}
If (@(Get-DnsServerZone $mailRRA.zonename ErrorAction SilentlyContinue ).Count -eq 0 ){
Write-Verbose "Creating DNS Zone: $($mailPZ.name)"
Add-DnsServerPrimaryZone @mailPZ
}
Else{
Write-Verbose "DNS Zone $($mailPZ.name) already exists"
}
If (@(Get-DnsServerZone $webmailRRA.zonename ErrorAction SilentlyContinue ).Count -eq 0 ){
Write-Verbose "Creating DNS Zone: $($webmailPZ.name)"
Add-DnsServerPrimaryZone @webmailPZ
}
Else{
Write-Verbose "DNS Zone $($webmailPZ.name) already exists"
}
If (@(Get-DnsServerZone $srvRRA.zonename ErrorAction SilentlyContinue ).Count -eq 0 ){
Write-Verbose "Creating DNS Zone: $($srvPZ.name)"
Add-DnsServerPrimaryZone @srvPZ
}
Else{
Write-Verbose "DNS Zone $($srvPZ.name) already exists"
}
#Get string equivalent of all A records
$RRA = Get-DnsServerResourceRecord ZoneName "autodiscover.$($fqdn)" RRType A | Out-String
$RRB = Get-DnsServerResourceRecord ZoneName "mail.$($fqdn)" RRType A | Out-String
$RRC = Get-DnsServerResourceRecord ZoneName "webmail.$($fqdn)" RRType A | Out-String
$RRD = Get-DnsServerResourceRecord ZoneName "_autodiscover._tcp.$($fqdn)" RRType Srv | Out-String
$ServerIPAddress | ForEach-Object {
If (!$RRA.Contains($_)){
Write-Verbose "Adding resource record $_ to $($dnsPZ.name)"
Add-DNSServerResourceRecordA @dnsRRA IPv4Address $_
}
Else{
Write-Verbose "Resource record $_ exists in $($dnsPZ.name)"
}
If (!$RRB.Contains($_)){
Write-Verbose "Adding resource record $_ to $($mailPZ.name)"
Add-DNSServerResourceRecordA @mailRRA IPv4Address $_
}
Else{
Write-Verbose "Resource record $_ exists in $($mailPZ.name)"
}
If (!$RRC.Contains($_)){
Write-Verbose "Adding resource record $_ to $($webmailPZ.name)"
Add-DNSServerResourceRecordA @webmailRRA IPv4Address $_
}
Else{
Write-Verbose "Resource record $_ exists in $($webmailPZ.name)"
}
}
$srvRRA | ForEach-Object {
If (!$RRD.Contains($($srvRRA.DomainName))){
Write-Verbose "Adding resource record $($srvRRA.DomainName) to $($srvPZ.name)"
Add-DNSServerResourceRecord Srv @srvRRA
}
Else{
Write-Verbose "Resource record $($srvRRA.DomainName) exists in $($srvPZ.name)"
}
}
}
END{}

What??? This wasn’t the code I expected! Turns out Ofir had a lot more he needed to configure. Just pointing him in the right direction was sufficient to generate this! Awesome! Give a man a fish… 😉

Go Ofir! It’s fun to see colleagues get excited about PowerShell. Sometimes all that’s needed is just a nudge in the right direction…

Ttyl,

Urv