PowerShell Profiles and why I don’t use them

The idea behind a PowerShell profile is that you can customise your PowerShell environment and have your system remember the setup the next time you open a PowerShell prompt / ISE.

What happens when you send your script to someone else?

It’s actually quite a cool idea and you can make sure all your PowerShell modules are loaded in the profile too.  The problem I see with this is that now your script has some hidden dependencies.  These are the modules etc. that you loaded into your PowerShell profile.

Unless the person you send the script to has the same modules loaded in their PowerShell profile the script won’t work

I find it better to just add the couple of lines to import the module into every script I write.  This makes sure that the script is portable assuming of course that the modules are available on the the users system where they are running the script.  You could of course write more code to download and install the module but lets not get carried away just write a message to the screen explaining why the script won’t run and let them source the required modules.

I use this function in my scripts and then handle the return value in my main script

 

function Get-ModuleStatus { # Version 2.00  
 [CmdletBinding()]
 param (
  [parameter(Mandatory=$true , HelpMessage="Enter the Module Name, e.g. ActiveRolesManagementShell")]
  [string]$name,
  [parameter(Mandatory=$false, HelpMessage="Optionally Enter the Version Number, e.g. 7.2")]
  [string]$Version,
  [switch]$forceVersion  
 )
 if ( $version ) { 
  if ( $forceVersion ) {
   if ( $module = Get-Module -Name $name | where { $_.version.ToString() -eq $Version } ) { Return $true }
  }
  else {
   if ( $module = Get-Module -Name $name | where { $_.version.ToString() -ge $Version } ) { Return $true }
  }
  if ( $module = Get-Module -name $name ) {
   # wrong version loaded so unload 
   Remove-Module -Name $name 
   $module = $null 
  }
 }
 elseif ( $module = Get-Module -name "$name" ) { Return $true }
 if ( $version ) { 
  try { Import-Module -Name $name  -MinimumVersion $version | Out-Null  }
  catch { return $false	}
 }
 else {
  try { Import-Module -Name "$name" } 
  catch { return $false	}
 }
 Return $true 
}           # Get-ModuleStatus           Version 2.00

here is an example of how to call and handle the error

if ( ( Get-ModuleStatus "ActiveRolesManagementShell" ) -eq $false ) { # load the quest cmdlets
 $message = "ActiveRolesManagementShell could not be loaded SCRIPT HALTING on $($Env:COMPUTERNAME) - Please investigate"
 $emailParameters.Add("body",$message)
 $emailParameters.Add("Subject","$scriptName Script FATAL ERROR - Unable to Load ARS COMMANDLETS on $($Env:COMPUTERNAME)")
 Stop-ScriptRun -emailParameters $emailParameters -sendMail -stop -throwMessage "FATAL ERROR - Unable to Load ARS COMMANDLETS"
} # throw if we are unable to load the Quest cmdlets
Advertisements

Forcing Admins to do the right thing

No I don’t mean enrolling them in the ‘Guido school of Admins‘ I mean getting them to do the right thing without even telling them a thing.  I mean preventing bad behaviour by using features of ActiveRoles Server.

Moving Objects around in AD can cause major issues!

Moving objects around in AD can cause major issues especially with LDAP based applications which always seem to , hard code the object DNs.

ARS Managed Units can present a virtualised OU structure – Bruce Lee might have referred to this by saying  “The art of Moving objects without moving objects”  or was it the “Art of fighting without fighting” – Enter the Dragon.

ARS Managed Units allow you to virtualise an AD OU Structure making it simpler to delegate rights and also provides a more logical view of AD and of course a less cluttered view for the admins as they only see the objects they need to manage.

Another advantage is that administrators cannot create new objects in a Managed Unit. A managed Unit can contain an OU but you can’t create objects in it.

What this means is that you can prevent an administrator creating a new parent OU and more importantly they can only move objects between the OUs you expose, i.e. when possible they can move the object from the ‘incorrect’ location to the correct OU location.

You could achieve similar results with a complex delegation model but lets stop for a minute here, the AD is already a mess and you can’t move the objects around for fear of breaking things so here’s a solution that makes them appear in the right place when they are not.

The solution then is to create a Managed Unit that has a membership rule that includes only the immediate child objects of the targeted OU.

This simple LDAP query achieves this

(&(objectCategory=organizationalUnit)(street=DisplayOUinMU))

All I need to do now is set the street attribute on all the objects in the target OU using a simple powershell script and create an ARS policy that ensures that any objects created or moved to the OU get this value too which is a very simple PVG policy.  The policy sets the attribute to the parentOU attribute value.

Then I set some additional membership rules to display any objects not in the target OU, i.e. are in the wrong OU path and Bobs your uncle.

Now when an admin users looks, he sees all the objects in the right location, almost anyway 🙂 The objects in the target OU ( the Managed Unit with the same name as the Target OU) are mostly from the wrong locations BUT the admin can no longer create more objects in that location only in the correct OUs exposed via the MU.

This is driving the correct behavior with little management or even training overhead. Without this I found that admins, despite being told time and time again, would create more objects in the wrong OUs because related groups or service accounts were already in the wrong OU location.

Putting objects in an OU ‘just because’ is up there with cloning a user object for the new start ARRRRGGGHHHHH don’t do it! ( Please start working on a roles based design today)

I’ve requested a Product ENHANCEMENT with OneIdentity ( Quest ) as I think it would be good to have this as an an include rule in a similar way to the include group member rule.   Then you would not need the PVG policy to set the attribute you would just include immediate child objects of the targeted OU.  Even more useful would be the ability to further filter that to specific object types, e.g. just the users or groups or perhaps just the OUs.  Maybe they will include it in the next release you never know.

ARS policy not working on Deprovisioned accounts

I just discovered, well actually another OneIdentity forum user (OneIdentity Employee) discovered, how to get ARS polices to apply to Deprovisioned accounts.

I’d thought for ages that all the policies stopped working when the account was deprovisioned. Generally this has been fine and I’ve not been bothered by it but a couple of times the SD have enabled a deprovisioned account.

The problem now is, that none of the ARS policies work on this account or so I thought and this can be a big problem especially if the account in question is a personal functional account that used to be linked to a personal employee account. As part of the leaver process all related accounts to the personal employee account are disabled and deprovisioned together and most of the attributes are cleared. Enable that account and it’s no longer linked to the HR data feed, i.e the JLT process is broken. As none of the policies apply any restriction or automation also does not work on this account and of course the deprovision option can’t be selected as it’s already been deprovisioned.

It would be a good feature if ARS prevented an account being enabled when it was in the deprovisioned state. We can of course use an ARS policy to do this but wait, ARS policies don’t work on deprovisioned accounts or do they?

it turns out that a workflow works regardless of the deprovision status and a deprovisioning policy also works on deprovisioned accounts, I always wondered why they has a provisoning and deprovisiong choice when creating a new policy. Now I know. Clearly there are overheads in watching every change to objects in AD so I’m guessing that to reduce the load they decided that in most instances why would we want to trigger an ARS policy on a deprovisioned account so they created 2 policy types. Provisioning only works on accounts where the edsvaDeprovisionStatus attribute is not present and Deprovisioning polices work on both.

This actually opens up a whole new way of thinking about the polices you want to apply and when.

I already have a script that prevents the SD enabling an account if it was disabled by the HR data feed. The quick fix then is to recreate that as a deprovisioning policy and now the SD can’t enable the account in any other way than by undo deprovision. Then all the required identity attributes will be restored, all the ARS policies apply and every one is happy, for a while at least 🙂

By the way did you also know that by default Managed Units do not contain deprovisioned users but there is a membership rule, right at the bottom of the list ‘Maintain deprovisioned Users’ that adds them back in.

I think the only things that can’t be configured to work on deprovisioned accounts are group family and dynamic groups, and I see no reason why I would want to override that rule anyway.

Extracting Photos from AD

This post is actually about the ‘DontConvertValuesToFriendlyRepresentation’ switch on get-qaduser but I came across this because I was trying to extract the photos from AD so that’s how I ended naming the post Extracting Photos from AD as most people will probably be search for this and not the command line switch.

Getting the photo from AD is pretty simple but there are a couple of things to know. When you upload the photo to AD it’s converted from a jpeg to a array of bytes so you can’t just download it you have to convert it back. the Quest commandlets are helpful and covert lots of the raw data stored in AD into more readable formats. What this means is that sometimes the help is more of a hindrance because the value you wanted for the photo has been converted so then the byte conversion fails ‘[System.Io.File]::WriteAllBytes( $Filename,$photoAsBytes )’

There are two solutions to this. The first is to just access the directory entry like this ‘$user.DirectoryEntry.thumbnailPhoto.Value’ and the second is to tell the commandlet not to convert the values by using the ‘DontConvertValuesToFriendlyRepresentation’ switch.

And as I was comparing the speed of the AD commandlets I extracted the thumbnailPhoto attribute using both the AD and Quest commandlets. The AD commandlets are faster but not by much as long as you use the ‘-DontUseDefaultIncludedProperties’ The quest commandlets pull down lots of attributes which is why it takes longer so when getting lots of AD objects it’s worth using this switch too.


cls
$ldapFilter = "(&(employeeID=*)(sAMAccountType=805306368)(thumbnailPhoto=*)(!(|(userAccountControl:1.2.840.113556.1.4.803:=2))))"
$searchRoot = "OU=User Accounts,DC=MyADDomain,DC=com"
$useADCommandlets = $false 
$sizelimit = 0
$OutputPath = 'c:\Temp\Photos'
Function ConvertTo-Jpeg {
 param ($userName,$photoAsBytes,$path='c:\temp')
 if ( ! ( Test-Path $path ) ) { New-Item $path -ItemType Directory }
 $Filename="$($path)\$($userName).jpg"
 [System.Io.File]::WriteAllBytes( $Filename,$photoAsBytes )
}

if ( $useADCommandlets ) {
 #Import-Module ActiveDirectory
 $Users = GET-ADUser -LDAPFilter $ldapFilter  -Properties thumbnailPhoto # | select -First $sizelimit # remove the select to get all users 
 ForEach ( $User in $Users ) {
  ConvertTo-Jpeg -userName $user.SamAccountName -photoAsBytes $user.thumbnailPhoto -path $OutputPath 
 }
}
else {
 $Users = get-qaduser  -LdapFilter $ldapFilter -SearchRoot $searchRoot -DontUseDefaultIncludedProperties -DontConvertValuesToFriendlyRepresentation  -IncludedProperties thumbnailphoto -SizeLimit $sizelimit   # set sizelimit to 0 to get all users
 ForEach ( $User in $Users ) {
  #ConvertTo-Jpeg -userName $user.SamAccountName -photoAsBytes $user.DirectoryEntry.thumbnailPhoto.Value -path $OutputPath # if you didn't use the -DontConvertValuesToFriendlyRepresentation switch 
  ConvertTo-Jpeg -userName $user.SamAccountName -photoAsBytes $user.thumbnailPhoto -path $OutputPath
 }
}

Splitting a long string into lines

This is a very simple exercise, probably why googling it didn’t get any hits as most people are interested in splitting text read in from a file. I wrote an audit script that would write
to the ARS scheduled task ‘lastrunmessage’ attribute so that I could quickly see if there were any errors in the last run.

I then wrote an audit script to audit the audit script, as I like to be sure my scripts are running error free.

I’d like to say I don’t have any bugs in my production scripts but we both know that’s not true! Plus things can change so a script that’s has run for years might suddenly stop. I had this exact issue with my scripts sending to an SMTP relay but that’s another post to come next.

Sometimes the script would generate multiple error messages so I stick them all together on the last run message but I discovered that the email alert generated was a little messy so I
wanted to split the looooooooooooooooooooooooooong lastrunmessage into a number of lines.

A quick function later and it was done. I didn’t even have to write a recursive function. The function has two parameters: stringLength, this is the length of the lines I want produce and string, which is the string I want to split up into lines.

I could have course added CRLF into the string and reconstituted the string but here all I wanted was an array of stings where the length didn’t exceed the stringLength parameter. I split the input sting using the ‘space’ as the delimiter.

$words = $string.split(" ")

Then I iterate through the words keeping any eye on the string length

ForEach ( $word in $words ) {
  if ( ( $newString.length + $word.length ) -gt $stringLength ) {

and rebuild the string as I go

  $newString += " $word "

until I hit the ‘stringLength’ limit when I add the current ‘newString’ to the array of ‘strings’ that the function will return.

  if ( ( $newString.length + $word.length ) -gt $stringLength ) {
   $strings += $newString
   $newString = $word
  }

the function will always return an array even if the input string is under the stringLength limit.

Here’s the function and an example command line to make use of the function.

Function Create-StringArray {
 param (
  $stringLength = 100,
  $string
 )
 $strings = @()
 if ( $string.Length -le $stringLength ) {
  $strings += $string 
  Return ,$strings # string is not big enough to other splitting so just return it as an array of 1 string
 }
 $words = $string.split(" ")
 $newString = ""
 ForEach ( $word in $words ) {
  if ( ( $newString.length + $word.length ) -gt $stringLength ) {
   $strings += $newString
   $newString = $word
  }
  elseif ( $newString -eq '' ) {
   $newString = $word 
  }
  else { 
   $newString += " $word "
  }
 }
 if ( $newString -ne '' ) {
  $strings += $newString
 }
 Return ,$strings 
}

$inputString = "this is a long string that is made up of words that would make it too long to display in a file without bad word wrapping happening"
ForEach ( $line in $(Create-StringArray -string $inputString) ) {
 $line 
}

How to check a GPO drive mapping

Rather than use logon scripts you can configure drive mappings in a User Preference GPO.  As with logon scripts the issue here is that over time you add to the drive mappings and never get around to checking if the drive mappings are still relevant.    If you have a large number of mappings this can be a big problem.  My solution is to the GrpupPolicy modules and extract an XML report from the Drive Mapping GPO and then parse the XML to confirm that the filters being used and the file paths are still valid.  Exporting the output to a CSV file is an easy way to document the GPO settings and also to highlight potential issues.

A HTML report could be used but in my book at least a CSV file can be opened in Excel and then sorted to show the drive mapping info required.  This is much better than the HTML report.

You might think its easy to examine an XML file and to some extent it is, but if you don’t know the structure then just dumping out the information is about all you can do and this is no better than jst reading the XML file which you probably know if not very much fun.

Using the GPO commandlets you can extract the drive mappings like this – I used Powergui to navigate trhough the XML to find the required nodes I was interested in and the HTML reports to help me work out what information I’d need.

$([xml]$(Get-GPOReport -Name $GPOName -ReportType Xml)).GPO.User.extensionData[0].Extension.DriveMapSettings

The drive mappings are returned as an array of mappings so you can walk through them one at a time using a For loop and for each one you can extract the filters and collections.  Collections are groups of filters that are evaluated together and you can AND or OR the collections too if the mapping has more than one collection.  Mosy in my scenario had only one collection but I wrote code to handle the one GPO that had two collections.

Having got the drive mappings and when looping through them I grabbed the filters and collections.  You can have one or both of these by the way

$driveMapSetting = $driveMapSettings.Drive[$index]
$driveMapSettingFilters = $driveMapSetting.Filters
$NoOfCollections = Count-Filters $driveMapSettingFilters.FilterCollection

There are four branches of the XML I’m interested in , users , groups, OUs and sites.  These are filters based on the object type so if a drive mapping were set if the users is a member of a group then there would be a FilterGroup element in the XML.

$userFilters = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterUser)
$groupFilters = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterGroup)
$OUfilters = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterOrgUnit)
$siteFilters = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterSite)

If there is no matching branch then the ‘count’ is set to 0 and I can use this to conditionally explore the XML node more or not.

if ( $groupfilters -gt 0 ) {
$filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection.FilterGroup “Group”
}

I wrote functions to simplify things and given I need to do the same thing over and over functions are definitely the way to go.

Now that I have located a filter all I need to do is store it in a variable but before I can do that I need to figure out the attributes I need.  The Powergui variable window is great for parsing the xml and working out what you need.

Now the last bit is the real labour saving bit, when I discover a group, user, OU or site in a filter I can check in AD if it exists.  If not then I can write an error message to the file.  I found lots of errors in my GPO so the time writing the script to automate the check paid off and that was a one time run.  Using it going forward will ensure the GPO does not get stale ever or at minimum I have no excuse.

#region User Configurable Parameters
$GPOName          = "GPOName"
$exportFolder     = "c:\temp"
$exportFileName   = "$exportFolder\$GPOName - Report.csv" 
$padright         = 110  # used to manage the oputput display length 
$limitDriveMapsTo = 0 # limits the number fo drive maps for debug only set to 0 for no limit
#endregion User Configurable Parameters
#region Define message coulour variables 
$script:Green     = @{"ForegroundColor"="green" ;"BackgroundColor"="Black"}
$script:Gray      = @{"ForegroundColor"="Gray"  ;"BackgroundColor"="Black"}
$script:Yellow    = @{"ForegroundColor"="Yellow";"BackgroundColor"="Black"}
$script:Cyan      = @{"ForegroundColor"="Cyan"  ;"BackgroundColor"="Black"}
#endregion Define message coulour variables  
#region Helper Functions
function Get-ModuleStatus { # http://www.ehloworld.com/938
	[CmdletBinding(SupportsShouldProcess=$true)]
	param (
		[parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true, HelpMessage="Enter the Module Name, e.g. Lync")]
		[string]$name
	)
	if(!(Get-Module -name "$name")) {
		if( Get-Module -ListAvailable | ? {$_.name -eq "$name"} ) {
			try { 
				Import-Module -Name "$name" 
				return $true
			}
			catch { return $false	}
		} 
		else { return $false }
	}
	else { return $true	}
} # end function Get-ModuleStatus
function Get-SnapinStatus { 
 [CmdletBinding(SupportsShouldProcess=$true)]
 param	(
  [parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true, HelpMessage="Enter the snapin name, e.g. Microsoft.Exchange.Management.PowerShell.Admin")]
  [string]$name
 )
 if(!(Get-PSSnapin -name "$name" -ErrorAction SilentlyContinue)) {
  if(Get-PSSnapin -Registered | Where-Object {$_.name -eq "$name"}) {
   try { 
    Add-PSSnapin -Name "$name" -ErrorAction SilentlyContinue 
    return $true
   }
   catch {	return $false }
  } 
  else { return $false }
 }
 else { return $true	}
}                    # Get-SnapinStatus and optionally load it into memory
Function Convert-OutputForCSV {
    <#
        .SYNOPSIS
            Provides a way to expand collections in an object property prior
            to being sent to Export-Csv.

        .DESCRIPTION
            Provides a way to expand collections in an object property prior
            to being sent to Export-Csv. This helps to avoid the object type
            from being shown such as system.object[] in a spreadsheet.

        .PARAMETER InputObject
            The object that will be sent to Export-Csv

        .PARAMETER OutPropertyType
            This determines whether the property that has the collection will be
            shown in the CSV as a comma delimmited string or as a stacked string.

            Possible values:
            Stack
            Comma

            Default value is: Stack

        .NOTES
            Name: Convert-OutputForCSV
            Author: Boe Prox
            Created: 24 Jan 2014
            Version History:
                1.1 - 02 Feb 2014
                    -Removed OutputOrder parameter as it is no longer needed; inputobject order is now respected 
                    in the output object
                1.0 - 24 Jan 2014
                    -Initial Creation

        .EXAMPLE
            $Output = 'PSComputername','IPAddress','DNSServerSearchOrder'

            Get-WMIObject -Class Win32_NetworkAdapterConfiguration -Filter "IPEnabled='True'" |
            Select-Object $Output | Convert-OutputForCSV | 
            Export-Csv -NoTypeInformation -Path NIC.csv    
            
            Description
            -----------
            Using a predefined set of properties to display ($Output), data is collected from the 
            Win32_NetworkAdapterConfiguration class and then passed to the Convert-OutputForCSV
            funtion which expands any property with a collection so it can be read properly prior
            to being sent to Export-Csv. Properties that had a collection will be viewed as a stack
            in the spreadsheet.        
            
    #>
    #Requires -Version 3.0
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline)]
        [psobject]$InputObject,
        [parameter()]
        [ValidateSet('Stack','Comma')]
        [string]$OutputPropertyType = 'Stack'
    )
    Begin {
        $PSBoundParameters.GetEnumerator() | ForEach {
            Write-Verbose "$($_)"
        }
        $FirstRun = $True
    }
    Process {
        If ($FirstRun) {
            $OutputOrder = $InputObject.psobject.properties.name
            Write-Verbose "Output Order:`n $($OutputOrder -join ', ' )"
            $FirstRun = $False
            #Get properties to process
            $Properties = Get-Member -InputObject $InputObject -MemberType *Property
            #Get properties that hold a collection
            $Properties_Collection = @(($Properties | Where-Object {
                $_.Definition -match "Collection|\[\]"
            }).Name)
            #Get properties that do not hold a collection
            $Properties_NoCollection = @(($Properties | Where-Object {
                $_.Definition -notmatch "Collection|\[\]"
            }).Name)
            Write-Verbose "Properties Found that have collections:`n $(($Properties_Collection) -join ', ')"
            Write-Verbose "Properties Found that have no collections:`n $(($Properties_NoCollection) -join ', ')"
        }
 
        $InputObject | ForEach {
            $Line = $_
            $stringBuilder = New-Object Text.StringBuilder
            $Null = $stringBuilder.AppendLine("[pscustomobject] @{")

            $OutputOrder | ForEach {
                If ($OutputPropertyType -eq 'Stack') {
                    $Null = $stringBuilder.AppendLine("`"$($_)`" = `"$(($line.$($_) | Out-String).Trim())`"")
                } ElseIf ($OutputPropertyType -eq "Comma") {
                    $Null = $stringBuilder.AppendLine("`"$($_)`" = `"$($line.$($_) -join ', ')`"")                   
                }
            }
            $Null = $stringBuilder.AppendLine("}")
 
            try { Invoke-Expression $stringBuilder.ToString() }
            catch { 
             $bp = 0 
            }
        }
    }
    End {}
}
Function Write-DriveMapGeneralProperties {
 param ( $driveMapSetting )
 Write-Host "".padright($padright,"=") @script:Green
 Write-Host "Order                : $($driveMapSetting.GPOSettingOrder)".padright($padright) @Gray
 Write-Host "Name                 : $($driveMapSetting.name)".padright($padright)  @Gray
 switch ( $($driveMapSetting.Properties.action) ) {
  "C"     { $action = "Create"    }
  "R"     { $action = "Reconnect" }
  "D"     { $action = "Delete"    }
  Default { $action = $($driveMapSetting.Properties.action) }
 }
 Write-host "Action               : $action".padright($padright) @Gray
 Write-Host "Hide/Show this drive : $($driveMapSetting.Properties.thisDrive)".padright($padright) @Gray
 Write-Host "Hide/Show all drives : $($driveMapSetting.Properties.allDrives)".padright($padright) @Gray
 if ($($driveMapSetting.Properties.persistent) -eq 0 ) {
  $persistent = "DISABLED"
 }
 else {
  $persistent = "ENABLED"
 }
  if ($($driveMapSetting.Properties.useLetter) -eq 0 ) {
  $useLetter = "DISABLED"
 }
 else {
  $useLetter = "ENABLED"
 }
 Write-Host "Reconnect/Persistent : $persistent".padright($padright) @Gray
 Write-host "UserName             : $($driveMapSetting.Properties.userName)".padright($padright) @Gray
 $path = $driveMapSetting.Properties.path
 $path = $path.replace("\%username%","") 
 $Error.Clear()
 $pathConnection = ""
 try { $pathAvailable = Test-Path $path }
 catch { }
 if ( $Error ) {
  $colours = $script:Yellow 
  if ( $Error.Exception.message -eq "Access is denied" ) {
   $pathConnection = " - ERROR: Access is denied"
  }
  else {
   $pathConnection = " - ERROR: Path not reachable"
  }
 }
 else {
  if ( $pathAvailable ) {
   $colours = $script:Gray
  }
  else {
   $pathConnection = " - ERROR: Path not reachable"
   $colours = $script:Yellow
  }
 }
 write-host "Path                 : $($driveMapSetting.Properties.path)$pathConnection".padright($padright) @colours
 Write-host "Label as             : $($driveMapSetting.Properties.label)".padright($padright) @Gray
 Write-host "useLetter            : $userLetter".padright($padright) @Gray
 Write-host "Letter               : $($driveMapSetting.Properties.letter)".padright($padright) @Gray
 Write-Host "".padright($padright,"-") @script:Green 
 Return $pathConnection
}
Function Count-Filters {
 param ($filters)
 $noOfFilters = 0 
 if ( $filters -ne $null ) {
  $noOfFilters = $filters.count
  if ( $noOfFilters -eq $null ) {
   $noOfFilters = 1
  }
 }
 $noOfFilters
}
Function Get-DriveMappings {
 param ( $GPOName )
 $([xml]$(Get-GPOReport -Name $GPOName -ReportType Xml)).GPO.User.extensionData[0].Extension.DriveMapSettings
 #$driveMaps.GPO.User.extensionData[0].Extension.DriveMapSettings.Drive # - will list all the drive maps in the GPO 
}
Function Count-DriveMappings {
 param ( $driveMappings ) 
 $NoOfMappings = $driveMappings.Drive.count
 if ( $NoOfMappings -eq $null ) {
  if ( $driveMappings.Drive -ne $null ) {
   $NoOfMappings = 1
  }
 }
 $NoOfMappings
}
Function Get-Filters {
 param ( $filters,$filterType)
 $filterSettings = @()
 ForEach ( $filter in $filters ) {
  $objectMissing = "'ERROR: Object Not found'"
  $objectType = $filterType
  switch ( $filterType ) {
   "Site" {
    if ( $script:Sites -contains $($filter.name) ) {
     $objectMissing = ""
    }
    Break
   }
   "User" {
    if ( Get-QADUser -SamAccountName $($filter.name) -Enabled -SearchRoot $script:searchRoot  ) {
     $objectMissing = ""
    }
    Break 
   }
   "Group" {
    if ( Get-QADGroup $($filter.name) -SearchRoot $script:searchRoot  ) {
     $objectMissing = ""
    }
    Break 
   }
   Default {
    if ( Get-QADObject $($filter.name) -SearchRoot $script:searchRoot -Type organizationalUnit  ) {
     $objectMissing = ""
    }
    Break 
   }
  }
  if ( ( $filter.userContext ) -and ( $filter.userContext -ne 1 ) ) {
   $bp = 0 
  }
  <# other settings that may be of interest 
   userContext,primaryGroup,localGroup - but would require a way of reporting this
  #>
  $filterSetting = "$($filter.bool) $(if ( $filter.not -eq 1 ){'NOT'}) $filterType = $($filter.name) $objectMissing "
  if ( $objectMissing -ne "" ) {
   $colours = $script:Yellow
  }
  else {
   $colours = $script:gray
  }
  Write-Host $filterSetting.padright($padright) @colours 
  $filterSettings += $filterSetting 
 }
 if ( $filterSettings.Count -ge 1 ) {
  Write-Host "".padright($padright,"-") -ForegroundColor Green -BackgroundColor Black
 }
 Return ,$filterSettings
}
Function Set-DriveMapping {
 param ( $driveMapSetting,$filterSettings,$pathConnection)
 $driveMapping = "" | select order,name,Action,useLetter,Letter,path,label,condition,'Hide/Show this drive','Hide/Show all drives','Reconnect/Persistent',UserName
 $driveMapping.order = $($driveMapSetting.GPOSettingOrder) 
 $driveMapping.name = $($driveMapSetting.name)
 switch ( $($driveMapSetting.Properties.action) ) {
  "C"     { $action = "Create" }
  Default { $action = $($driveMapSetting.Properties.action) }
 }
 $driveMapping.Action = $action
 $driveMapping.Letter = $($driveMapSetting.Properties.Letter)
 $driveMapping.path = "$($driveMapSetting.Properties.path) $pathConnection".trim() 
 $driveMapping.label = $($driveMapSetting.Properties.label)
 $driveMapping.condition = $filterSettings
 $driveMapping.'Hide/Show this drive' = $($driveMapSetting.Properties.thisDrive)
 $driveMapping.'Hide/Show all drives' = $($driveMapSetting.Properties.allDrives)
 if ($($driveMapSetting.Properties.persistent) -eq 0 ) {
  $persistent = "DISABLED"
 }
 else {
  $persistent = "ENABLED"
 }
  if ($($driveMapSetting.Properties.useLetter) -eq 0 ) {
  $useLetter = "DISABLED"
 }
 else {
  $useLetter = "ENABLED"
 } 
 $driveMapping.'Reconnect/Persistent' = $($driveMapSetting.Properties.persistent)
 $driveMapping.useLetter = $useLetter
 $driveMapping.UserName = $($driveMapSetting.Properties.userName)
 $driveMapping 
}
#endregion Helper Functions
#region load required powershell modules
if ( ( Get-ModuleStatus "ActiveRolesManagementShell" ) -eq $false ) { # load the quest cmdlets
 Write-Host "You need to have the Quest ARS 6 or 7 commandlets installed for this script to work".padright($padright) @yellow 
 Write-host "Trying to load VARS 6 commandlets".padright($padright) @green
} # throw if we are unable to load the Quest cmdlets
elseif ( $(Get-SnapinStatus "Quest.ActiveRoles.ADManagement") -eq $false ) { # load the quest cmdlets
 	Throw "Quest.ActiveRoles.ADManagement could not be loaded SCRIPT HALTING - Please investigate"
} # throw if we are unable to load the Quest cmdlets
if ( ( Get-ModuleStatus "GroupPolicy" ) -eq $false ) { # load the quest cmdlets
 Throw "FATAL ERROR - Unable to Load GroupPolicy COMMANDLETS"
} # throw if we are unable to load the Quest cmdlets
#endregion load required powershell modules
#region get AD Sites
$script:searchRoot = "DC=myDomain,DC=com"
$connection        = Connect-QADService "mydomain.com"
$configNCDN        = (Get-QADRootDSE -Connection $connection).ConfigurationNamingContext
$siteContainerDN   = ("CN=Sites," + $configNCDN)
$script:Sites      = Get-QADObject `
                     -SearchRoot $siteContainerDN `
                     -LdapFilter "(objectClass=site)" `
                     -IncludedProperties siteObjectBL,location,description | 
                     select -ExpandProperty Name #,Location,Description,siteObjectBL 
#endregion get AD Sites
#region Process Drive Maps 
cls
$driveMapSettings      = Get-DriveMappings $GPOName
if ( ! ( $NoOfMappings = Count-DriveMappings $driveMapSettings ) ) {
 Write-Host "No Drive Mapping Found - Quitting Script".padright($padright) @yellow 
 exit
}
if ( $limitDriveMapsTo -gt 0 ) {
 $NoOfMappings = $limitDriveMapsTo # used to limit the bnumber of drive maps processed for debug 
}
$driveMappings = @() 
for ($index = 0; $index -lt $NoOfMappings; $index++) {
 #region Initialise loop variables and print drive map info 
 $filterSettings         = @()
 $driveMapSetting        = $driveMapSettings.Drive[$index] 
 $driveMapSettingFilters = $driveMapSetting.Filters
 $NoOfCollections        =  Count-Filters $driveMapSettingFilters.FilterCollection 
 $pathConnection = Write-DriveMapGeneralProperties $driveMapSetting  
 #endregion Initialise loop variables and print drive map info 
 #region Check if there are multiple collections defined 
 if ( $NoOFCollections -gt 1 ) {
  $collectionFilters = @()
  for ($indexColection = 0; $indexColection -lt $NoOfCollections; $indexColection++) {
   Write-Host "==========< $($driveMapSettingFilters.FilterCollection[$indexColection].bool) $(if ( $driveMapSettingFilters.FilterCollection[$indexColection].not -eq 1 ) { 'NOT' }) FilterCollection: $indexColection>==========".padright($padright) @cyan
   $filterSettings +=  "==========< $($driveMapSettingFilters.FilterCollection[$indexColection].bool) $(if ( $driveMapSettingFilters.FilterCollection[$indexColection].not -eq 1 ) { 'NOT' }) FilterCollection: $indexColection>=========="
   $userFilters  = $(Count-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterUser)
   $groupFilters = $(Count-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterGroup)
   $OUfilters    = $(Count-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterOrgUnit)
   $siteFilters  = $(Count-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterSite) 
   if ( $userfilters -gt 0 ) {
    $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterUser "User" 
   }  
   if ( $groupfilters -gt 0 ) {
    $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterGroup "Group" 
   }  
   if ( $OUfilters -gt 0 ) {
    $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterOrgUnit "OU"
   }
   if ( $siteFilters -gt 0 ) {
    $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection[$indexColection].FilterSite "Site"
   }
   $bp = 0 
   $filterSettings += "===========<End FilterCollection: $indexColection>==========="
  }
 } #if ( $NoOFCollections -gt 1 ) {
 #endregion Check if there are multiple collections defined 
 #region Check if there was a single collection defined 
 elseif ( $NoOfCollections -eq 1 ) {
  Write-Host "==========< $($driveMapSettingFilters.FilterCollection.bool) $(if ( $driveMapSettingFilters.FilterCollection.not -eq 1 ) { 'NOT' }) FilterCollection: 0>==========".padright($padright) @cyan
  $filterSettings +=  "==========< $($driveMapSettingFilters.FilterCollection.bool) $(if ( $driveMapSettingFilters.FilterCollection.not -eq 1 ) { 'NOT' }) FilterCollection: 0>=========="
  $userFilters  = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterUser)
  $groupFilters = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterGroup)
  $OUfilters    = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterOrgUnit)
  $siteFilters  = $(Count-Filters $driveMapSettingFilters.FilterCollection.FilterSite) 
  if ( $userfilters -gt 0 ) {
   $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection.FilterUser "User" 
  }  
  if ( $groupfilters -gt 0 ) {
   $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection.FilterGroup "Group" 
  }  
  if ( $OUfilters -gt 0 ) {
   $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection.FilterOrgUnit "OU"
  }
  if ( $siteFilters -gt 0 ) {
   $filterSettings += Get-Filters $driveMapSettingFilters.FilterCollection.FilterSite "Site"
  }
  $bp = 0 
  $filterSettings += "===========<End FilterCollection: 0>==========="
 } # elseif ( $NoOfCollections -eq 1 ) {
 #endregion Check if there was a single collection defined 
 #region process any other drive maps 
 $groupFilters = $(Count-Filters driveMapSettingFilters.FilterGroup)
 $OUfilters    = $(Count-Filters driveMapSettingFilters.FilterOrgUnit)
 $siteFilters  = $(Count-Filters $driveMapSettingFilters.FilterSite) 
 if ( $groupfilters -gt 0 ) {
  $filterSettings += Get-Filters $driveMapSettingFilters.FilterGroup "Group"
 }  
 if ( $OUfilters -gt 0 ) {
  $filterSettings += Get-Filters $driveMapSettingFilters.FilterOrgUnit "OU"
 }
 if ( $siteFilters -gt 0 ) {
  $filterSettings += Get-Filters $driveMapSettingFilters.FilterSite "Site"
 }
 $driveMappings +=  Set-DriveMapping $driveMapSetting $filterSettings $pathConnection 
 #endregion process any other drive maps
} # for ($index = 0; $index -lt $NoOfMappings; $index++) {
#endregion Process Drive Maps 
#region ExportResults 
$driveMappings | select order,name,Action,Letter,path,label,condition,'Hide/Show this drive','Hide/Show all drives','Reconnect/Persistent',UserName,UseLetter | Convert-OutputForCSV | Export-Csv $exportFileName -NoTypeInformation 
#endregion ExportResults