Listing the line management chain

It’s easy to get the direct reports of a line manager but what about the users who are in the line management chain but not direct reports of the line manager you are interested in.

Here’s a function that will take a DN of the line manager and then walk up the tree from there until it reaches the line manager you are interested in. You could query all of your users or as in this example, I’m interested in dividing up the users in a specific group into the line management chains of two specific line managers.

Function Get-ManagementChain {
 param ( $managerDN,$stopDNs)
 if ( $stopDNs -contains $managerDN ) { 
  Return $managerDN
 }
 $manager = Get-QADUser $managerDN -IncludedProperties manager | select -ExpandProperty manager 
 if ( $stopDNs -contains $manager ) {
  return $manager
 }
 elseif ( $manager ) {
  Get-ManagementChain $manager $stopDNs
 }
 else {
  return $null 
 }
}

# these are the attributes I want in my report include and output
$incAttrs = @(
 'manager'
)

$outAttrs = @(
 @{n="GroupName";e={$groupName}}
 'sAMAccountName'
 'NTAccountName'
 'DisplayName'
 'department'
 @{n="managementLine";e={Get-ManagementChain $_.manager $stopDNs}}
 'manager'
)

# Amazingly the array above is expanded for each user in the pipeline – You would think it would be set when the variable was instantiated but it’s not!

# This is the list of line managers I am interested in
$stopDNs = @(
 "CN=<MANAGEROFINTEREST1>,DC=<MYDOMAIN>,DC=com"
 "CN=<MANAGEROFINTEREST2>,DC=<MYDOMAIN>,DC=com"
)

$sizelimit = 0
$groupName = "MYGROUPOFINTEREST"

# and off we go almost a 1 liner powershell script 🙂

Get-QADGroupMember "MYDOMAIN\$groupName" -IncludedProperties $incAttrs -SizeLimit $sizelimit | 
 select $outAttrs |
 Export-Csv "c:\temp\UserList.csv" -NoTypeInformation

Schoolboy error using = instead of -eq in a conditional statement – not always!

I had a conversation the other day about powershell and the schoolboy error of using the vb script construct of = instead of –eq in a powershell conditional statement and I said that there is definitely a use case where it’s correct but at the time couldn’t think of a case where that was true.

Today I was asked to help someone debug a very simple script to get some information out of AD using a list of user names.

The input filename is just a list of usernames, not all of which exist in AD so the script needed to handle this cleanly.

This is an example where using = is a valid construct.

if ( $user = get-qaduser $_ -IncludedProperties $includedAttributes -Connection $proxy -SearchRoot $searchRoot )

What happens is, if there is an AD user object returned then the conditional statement is true and at the same time $user is set to AD User object. If no user is returned then It’s $null and the else clause will run.

In the actual example there were a lot of attributes required and this meant using the -IncludedPorperties switch and a long select statement with all the output variables including some text formatting using @{name=””;expression={}}

To make the code cleaner I used input and output array variables. I’d done this before and it makes the code look a lot cleaner but I’d never tried adding text formatting into the array. Amazingly it works even when using a conditional statement like this @{name=’Employee ID’;Expression={if ( $_.’employeeID’ -ne $null ){ $_.’employeeID’ } else { $_.’employeeNumber’ }}} – you’d think that would fail as you instantiate the array once not every time in the pipeline but it works, try it yourself.


cls
$searchRoot = "OU=User Accounts,DC=MyDomain,DC=com"
$inputFileName = 'c:\Temp\UserLogonNames.txt'
$outPutFileName = 'c:\Temp\OutputReportName.csv'

$includedAttributes = @(
'employeeID'
'employeeNumber'
'department'
)

$outPutAttributes =@(
@{name='Logon Name';expression='samaccountname'}
@{name='First Name';expression='givenName'}
@{name='Last Name';Expression='sn'}
@{name='Employee ID';Expression={if ( $_.'employeeID' -ne $null ){ $_.'employeeID' } else { $_.'employeeNumber' }}}
@{name='Department Name';expression='department'}
'DisplayName'
'AccountIsDisabled'
)

# The beauty of doing it this way is it’s easy to add and remove attributes later on by just managing the array variable
# and it makes the actual ‘payload’ command nice and short and it’s even easier to see the long list or attributes
# you are getting as a otherwise it looks like this

#Select @{name='Logon Name';expression='samaccountname'},@{name='First Name';expression='givenName'},@{name='Last Name';Expression='sn'},@{name='Employee ID';Expression={if ( $_.'employeeID' -ne $null ){ $_.'employeeID' } else { $_.'employeeNumber' }}},@{name='Department Name';expression='department'},,'DisplayName','AccountIsDisabled'


$nullUser = "" | Select 'samaccountname','givenName','sn','employeeID','department','DisplayName','AccountIsDisabled'
$nullUser.'givenName' = 'No AD Object Found'

Get-Content $inputFileName |
ForEach-object {
if ( $user = get-qaduser $_ -IncludedProperties $includedAttributes -Connection $proxy -SearchRoot $searchRoot ) {
$user
} else {
$nullUser."samaccountname" = $_ ; $nullUser }
} |
select $outPutAttributes #|
# Export-csv $outPutFileName -NoTypeInformation -Encoding "UTF8"

Updating the Password in ARS Managed Domains

I came across a post explaining how to do this and realised that for some of the managed domains the password was not being changed and these accounts were normally domain admins.

As per my last post on generating random passwords and the general discussion on if we should be forcing users to change their passwords thre is no question that it’s a good idea to automate a password reset on privileged service accounts.

ARS managed domains have the option of using either the service account that runs the ARS service or you can add an override account. If you don’t have a trust in place then an override account is the only option. For the service account that runs the ARS service you could probably use a managed service account. I’ve not done this as I sometimes have to resort to actually using this account to debug some of the ARS script policies. I use a password management tool to reset the password, update the servive configuration and then reset the service. For the override accounts I use an ARS scheduled task that changes the password in the target domain and then updates the managed domain configuration.

The excerpt of code below gets the Managed Domain Objects and selects only those where there is an override account configured. The ‘edsaUseOverrideAccount’ will be set to $true. I then iterate through them and get the ‘edsaAccountName’ which holds the NTAccount Name of the override account and I reset the domain account using ARS and then update the ‘edsaAccountPassword’ attribute of the Managed Domain object.

This appears to work well and I’ve not had any issues with it. Now nobody knows the account password and it will get reset on whatever schedule you want to run this task.

I’ve not posted my function ‘Stop-ScriptRun’ this just does some clean up and sends an email ( if instructed to do so ) and then uses ‘Throw’ to stop teh sctip running and this updated the last run message of the scheduled task so it’s easy to see what happened on the last run by just looking at the scheduled task.

$managedDomains = Get-QADObject -SearchRoot 'CN=Managed Domains,CN=Server Configuration,CN=Configuration' -Proxy -IncludedProperties edsaUseOverrideAccount,edsaAccountName  | where { $_.edsaUseOverrideAccount }
ForEach ( $ManagedDomain in $managedDomains ) {
 $pwd = Get-RandomPassword -MaxLength $pwdMAXLength -MinLength $pwdMINLength -Upper $pwdUPPER -Lower $pwdLOWER -number $pwdNUMBER -Special $pwdSPECIAL -alphaFirst:$alphaFirst -numericFirst:$numFirst  -showdebug:$debug
 try { Set-QADUser -Identity $ManagedDomain.edsaAccountName -UserPassword $pwd -Proxy -Control @{operationReason=$operationReason} -WhatIf:$whatif }
 catch { 
  Stop-ScriptRun -emailParameters $emailParameters -throwMessage "Unable to set password on account $($ManagedDomain.edsaAccountName)" -stop -sendMail 
 }
 try { Set-QADObject $ManagedDomain.DN -ObjectAttributes @{edsaAccountPassword=$pwd} -Control  @{operationReason=$operationReason} -Proxy -WhatIf:$whatif }
 catch {
  Stop-ScriptRun -emailParameters $emailParameters -throwMessage "Unable to update $($ManagedDomain.name) password, $($ManagedDomain.edsaAccountName)"  -stop -sendMail 
 }
}

Generate a Random Password

A quick google didn’t return too many interesting PowerShell scripts so I decided to revisit one of my own.

The jury is still out on if we should be changing passwords regularly ( or maybe not after reading the links below).  Forcing regular password changes just makes people use weak password and password systems which allow an attacker to decode all passwords having gained access to one.

Best practice is now not to change your password but use a strong ( complex strong not just long ) one instead.

Take a look at these links:

Even the National Cyber Security Centre don’t recommend you force people to change the password.

So having firmly established that we shouldn’t be changing passwords my next two blogs will be on how to generate a secure password ( well if we are going to change it then we might as well change it to a strong one ) and to automate the change 🙂

Often Privileged service accounts will have a non expiring password – Crazy!

All of these articles assume we are discussing end users not service accounts.  Systems don’t care if the password is horrendous to remember or type.  Service accounts are often privileged accounts and yet almost all service accounts have a password that to set to never expire.

Microsoft tacked on fine grained passwords ( and it feels like its tacked on too ) none of the existing management tools are even aware of it.  You need to be a domain admin to use the PowerShell commandlets to determine what the resultant password policy actually is for each user.   Most tools still just look at the default domain policy.

Microsoft have been trying to help with this issue with recent OS updates.  We can now set more than one password policy in a domain using Password Settings Objects and we can also use ‘managed service accounts’ for many of the Microsoft products.   Managed service accounts are great as no one knows the password ever and the system will change it for you on a regular basis.

Use Managed Service Accounts if you can but if not then extra strong passwords for service accounts should be used.  Use ARS to enforce then when an account is created by adding the account to a dynamic group linked to a fine grained password policy.

You should be using stronger password policies for all service accounts.  Although you could do this manually of course, using a Password Settings Object with a strong password policy is going to ensure someone doesn’t use a very easy to guess password on their service account once you hand it over to them.  There are other restrictions you can place on service accounts but that’s straying too far off piste for this blog article.

If you have ARS then you can use a dynamic group to ensure all new service accounts get the appropriate password policy.

Where possible use managed service accounts but what about the rest?  Applications that can’t use Managed Service accounts and the app support teams want a non expiring password.

This is where you need your Information Security Team to go to bat for you but if the app support team are allowed to have a static password on their service account then they should be required to change it regularly.  For windows based applications generally that’s not too difficult and there are some tools out there that can help. Many password vaults will change the password on a schedule and also update the service password on systems where the service account is used.  This is again a little off piste so lets get back to the main topic of the next two blog articles.  How to generate a secure password and then how to use this password to reset the password on an ARS managed domain.

My script function is self documented so I won’t really need to write too much about it.  Just use Get-Help to list out the script documentation.   I use  an arrays to store the various character sets and then use Get-Random to choose one of the characters.  There are 4 arrays in all, one for each character type, i.e. Upper Case letters , Lower case letters, numbers and Special characters.  I then combine them into a single array.  The script parameters control which arrays are combined, e.g. if I use -special 0 then the special characters are left out of the combined array of allowed characters.

I then needed a way of ensuring that a password contained the minimum number of each character set.    The way I did this was to create a while loop that keeps going until I have my password.  Inside the ‘While’ loop I select a random character from the combined set of characters  and compare it with the individual character sets and increment a counter so that the password would always have the minimum number of required character types in the password before padding the password to meet the length requirement.  If the charLimit is passed to the function then it will prevent repeat characters.

There is a slight problem here in that I just removed some of the randomness ( is that a word? ) as the first set of characters have a known formula, i.e. using the function defaults, by not adding any command line switches, means the first 4 characters will always be made up of 1 upper alpha, 1 lower alpha 1 numeric and 1 special.   This is hardly insecure but for good measure I wrote another bit of code to randomise the initial password generated by my code.  I take the initial password and copy it to another variable, converting it to a list which will allow me to access the Remove method.

Sadly the ‘Remove’ method of an array object does not work so you need to convert the array to a list if you want to remove characters from the array

I can now use a For loop and randomly select one of the initial password characters and start building a new password removing the character from the array list as I go.  This keeps my password complexity intact but also improves the randomness of the returned password.  If the  concurrentLimit parameter is used then this section of code also prevents repeating characters.

I also added a way of making the generated password more user friendly using a -friendly switch.  This reduces the character set by removing the ambiguous characters, e.g. l and I.  This is really designed for a single use password that will be used to reset a users password and you want to ensure when you communicate the password to the user that they don’t mistake some of the characters.  The password is definitely not as strong when you reduce the allowed character pool but as you will be setting the user must change password at next logon flag this isn’t a problem.

here is the full function:

Function Get-RandomPassword {
 <#   .SYNOPSIS   Generates a Random Password.   .DESCRIPTION   Generates a Random Password which has controllable complexity using command line switches.  The friendly mode is typically used   when generating a password for one time use only and avoids possible confusion over the characters by removing ambiguous    characters from the available pool, e.g. I and l look the same so are removed from the characters set used to build the    password.      The default password is complex with at least 8 characters including at least 1 upper, 1 lower, 1 number and 1 special character.      You can also limit the number of repeated and consecutive characters that are allowed although these actually make a password weaker    if it is known that this rule is used when generating passwords.  It is included in case this limitation is imposed by the system    you are generating the password for.   .OUTPUTS   A Password String   .PARAMETER MaxLength   [int] The Maximum length of the password to be returned.     .PARAMETER MinLength   [int] The minimum length of the password to be returned.   .PARAMETER Upper   [int] The MINIMUM number of UPPER case characters to be included in the password.       .PARAMETER Lower   [int] The MINIMUM number of LOWER case characters to be included in the password.       .PARAMETER Number   [int]   The MINIMUM number of NUMERIC characters to be included in the password.    .PARAMETER Special   [int]   The MINIMUM number of SPECIAL characters to be included in the password.       .PARAMETER CharLimit   [int]   Limits the number of times a character can appear in the password, e.g. aardvark is not allowed because 'a' and 'r' are repeated in the password.      .PARAMETER ConcurrentLimit   [int]   Limits the number of times a character can be used concurrently, e.g. 'P4ssword$' is not allowed becasue 'ss' is present in the password.       .PARAMETER Padright   [int] Used for debug only and determines the message padding when write-host is called      .PARAMETER alphaFirst   [switch] Forces first character to be an Alpha.       .PARAMETER numericFirst   [switch] Forces first character to be NUMERIC.       .PARAMETER showDebug   [switch] Used to display debug messages to screen.       .PARAMETER Friendly   [switch] Used to limit the characters set by removing the ambiguous characters, e.g. I and l .    .EXAMPLE    $password = Get-RandomPassword -MaxLength 20 -MinLength 10 -Upper 5 -Lower 5 -number 1 -Special 1     Returns a 12 to 20 Character password - because the MinLEngth must be at least equal to the character requirements    e.g. 5 + 5 + 1 + 1 = 12      .EXAMPLE    $password = Get-RandomPassword -Friendly      Returns a strong 8 character password - excluding any ambiguous characters       .EXAMPLE    $password = Get-RandomPassword -MaxLength 10 -MinLength 8  -Special 0    Returns a password 8 to 10 characters in length that does not include any special characters   .NOTES   FunctionName : Get-RandomPassword   Created by   : Lee Andrews   Date Coded   : 03/06/2017   #>
 param (
  [parameter(HelpMessage = 'The Maximum Length of the password to generate')]
  [int]$MaxLength  = 8,
  [parameter(HelpMessage = 'The Minimum Length of the password to generate')]
  [int]$MinLength  = 8,
  [parameter(HelpMessage = 'The Minimum number of the UPPER case characters to be added to the password')]
  [int]$Upper      = 1,
  [parameter(HelpMessage = 'The Minimum number of the LOWER case characters to be added to the password')]
  [int]$Lower      = 1,
  [parameter(HelpMessage = 'The Minimum number of the NUMBERS to be added to the password')]
  [int]$number     = 1,
  [parameter(HelpMessage = 'The Minimum number of the SPECIAL characters to be added to the password')]
  [int]$Special    = 1,
  [parameter(HelpMessage = 'The Maximum Times a character can be used in a password, 0 = no limit')]
  [int]$CharLimit   = 0,
  [parameter(HelpMessage = 'The Maximum number of concurrent characters that can be the same, 0 = no limit')]
  [int]$concurrentLimit  = 0,
  [parameter(HelpMessage = 'Used to pad debug messages')]
  [int]$padright   = 100,
  [parameter(HelpMessage = 'Used to force an ALPHA to the begining of the password')]
  [switch]$alphaFirst,
  [parameter(HelpMessage = 'Used to force an NUMERIC to the begining of the password')]
  [switch]$numericFirst,
  [parameter(HelpMessage = 'Used to display messages to screen')]
  [switch]$showDebug,
  [parameter(HelpMessage = 'Used to remove ambiguous characters from the available pool')]
  [switch]$friendly
 )

  Function Update-Password {
  param (
   [ref]$pwd,
   [ref]$foundChrType,
   $requiredCharCount,
   $requiredComplexityMet,
   $pwdChr
  )
  Function Check-ConcurrentLimtNotExceeded {
   param ($nextPWDChar)
   if ( $CharLimit -ne 0 ) {
    if ( $charCount.contains($nextPWDChar) ) {
     if ( $charCount.item($nextPWDChar) -eq $CharLimit ) {
      $combination.Remove($nextPWDChar)
      Return $true
     }
     else {
      $charCount.item($nextPWDChar) = $charCount.item($nextPWDChar) + 1
     }
    }
    else {
     $charCount.add($nextPWDChar,1)
    }
   }
   Return $false
  }
  if ( $requiredComplexityMet ) {
   if ( $foundChrType.value -gt 0 ) {
    if ( Check-ConcurrentLimtNotExceeded $pwdChr ) {
     return $true
    }
    $pwd.value += $pwdChr
    $foundChrType.value  ++
   }
   Return $true
  }
  if ( $foundChrType.value -lt $requiredCharCount  ) {
   if ( Check-ConcurrentLimtNotExceeded $pwdChr ) {
    Return $false
   }
   $foundChrType.value  ++
   $pwd.value += $pwdChr
   Return $false
  }
  Return $true
 } # Function Update-Password {
 #region Initialise Variables
 if ( $friendly ) {
  # use this set when setting a password with the password must change flag
  $uppers = 'A','C','E','F','H','K','M','N','T','W','X','Y'
  $lowers = 'a','b','c','d','e','f','g','h','k','m','n','r','s','t','w','x','y','z'
  $numerals = '3','4','7','9'
  $specials = '£','%','&','*','@','#','='
 }
 else {
  $uppers = 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
  $lowers = 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
  $numerals = '0','1','2','3','4','5','6','7','8','9'
  $specials = '£','%','&','*','@','#','='
 }
 $combinationList     = @()
 $totalRequiredLength = 0
 $foundUppers         = 0
 $foundLowers         = 0
 $foundNumerals       = 0
 $foundSpecials       = 0
 $pwd                 = ''
 $charCount           = @{}
 $concurrent          = @{}
 #endregion Initialise Variables
 #region Dynamically adjust variables based on password complexity requirements
 if ( $Upper   -ge 1 ) { $combinationList += $uppers   ; $totalRequiredLength += $Upper   ; $foundUpper   = $false } else { $foundUpper   = $true }
 if ( $Lower   -ge 1 ) { $combinationList += $lowers   ; $totalRequiredLength += $lower   ; $foundLower   = $false } else { $foundLower   = $true }
 if ( $number  -ge 1 ) { $combinationList += $numerals ; $totalRequiredLength += $number  ; $foundNumeral = $false } else { $foundNumeral = $true }
 if ( $special -ge 1 ) { $combinationList += $specials ; $totalRequiredLength += $special ; $foundSpecial = $false } else { $foundSpecial = $true }
 $combination = New-Object System.Collections.ArrayList($null)
 $combination.AddRange($combinationList)
 if ( $showDebug ) {
  Write-Host "".padright($padright,"-") -ForegroundColor Green -background Black
  Write-Host "Maximum Password Length = $MaxLength".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Minimum Password Length = $MinLength".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Total   Required Length = $totalRequiredLength".padright($padright) -ForegroundColor Green -background Black
 }
 if ( $totalRequiredLength -le 0          ) { Throw "ERROR password complexity rules does not include any character sets" }
 if ( $MaxLength           -lt $MinLength ) { Throw "Minimum password length cannot be less than maximum password length" }
 if ( $totalRequiredLength -gt $MaxLength ) { Throw "Password complexity requirement greater than the requested maximum password length" }
 if ( $MinLength -lt $totalRequiredLength ) {
  $MinLength = $totalRequiredLength
  if ( $showDebug ) {
   Write-Host "Min Password Length increased to match the larger minimum REQUIRED PWD length".padright($padright) -ForegroundColor Green -background Black
  }
 }
 $PwdLen = $rand.Next($MinLength,$MaxLength+1) # generate a random password length between min and max length parameters
 if ( $showDebug ) { Write-Host "Target  Password Length = $PwdLen".padright($padright) -ForegroundColor Green -background Black }
 #endregion Dynamically adjust variables based on password complexity requirements
 #region Build Initial password based on complexity requirements
 $loopRetries = 10000
 $loopTry = 0
 $lastPWDChar = ""
 while ( ( $PWD.length -lt $PwdLen ) -or ( ! ( $foundUpper -and $foundLower -and $foundNumeral -and $foundSpecial ) ) ) {
  if ( $loopRetry -gt $loopRetries ) { Throw "Unable to create password - compexity rules may be too restrictive" } else { $loopTry ++ }
  try { $nextPWDChar = Get-Random -InputObject $combination }
  catch {
   $bp = 0
   Throw "FATAL ERROR: Complexity Rules have exhausted the character pool"
  }
  #Write-Host "Next Chr = $nextPWDChar"

  switch ($nextPWDChar) {
  	{$uppers -ccontains $_} {
    $foundUpper = Update-Password -foundChrType ([ref]$foundUppers) -pwdChr $_ -pwd ([ref]$pwd) -requiredCharCount $Upper  -requiredComplexityMet $( $foundUpper -and $foundLower -and $foundNumeral -and $foundSpecial )
    break
  	}
  	{$lowers -ccontains $_} {
    $foundLower = Update-Password -foundChrType ([ref]$foundLowers) -pwdChr $_ -pwd ([ref]$pwd) -requiredCharCount $Lower  -requiredComplexityMet $( $foundUpper -and $foundLower -and $foundNumeral -and $foundSpecial )
  		break
  	}
  	{$numerals -ccontains $_} {
    $foundNumeral = Update-Password -foundChrType ([ref]$foundNumerals) -pwdChr $_ -pwd ([ref]$pwd) -requiredCharCount $number  -requiredComplexityMet $( $foundUpper -and $foundLower -and $foundNumeral -and $foundSpecial )
    break
  	}
  	{$specials -ccontains $_} {
    $foundSpecial = Update-Password -foundChrType ([ref]$foundSpecials) -pwdChr $_ -pwd ([ref]$pwd) -requiredCharCount $special  -requiredComplexityMet $( $foundUpper -and $foundLower -and $foundNumeral -and $foundSpecial )
    break
	  }
	  default {
    Throw "UNKNOWN CHARACTER in Random PASSWORD Script Cannot Continue"
	  	break
	  }
  }
  $lastPWDChar = $nextPWDChar
 } # while
 #endregion Build Initial password based on complexity requirements
 #region Optionally Show password complexity information
 if ( $showDebug ) {
  Write-Host "".padright($padright,"-") -ForegroundColor Green -background Black
  Write-Host "Min Uppers...: $Upper".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Min Lowers...: $Lower".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Min Numerals.: $number".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Min Specials.: $Special".padright($padright) -ForegroundColor Green -background Black
  if ( $alphaFirst ) {
   Write-Host "First must be: ALPHA".padright($padright) -ForegroundColor Green -background Black
  }
  if ( $numericFirst ) {
   Write-Host "First must be: NUMERIC".padright($padright) -ForegroundColor Green -background Black
  }
  Write-Host "".padright($padright,"-") -ForegroundColor Green -background Black
  Write-Host "Set Uppers...: $foundUppers".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Set Lowers...: $foundLowers".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Set Numerals.: $foundNumerals".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Set Specials.: $foundSpecials".padright($padright) -ForegroundColor Green -background Black
  Write-Host "".padright($padright,"-") -ForegroundColor Green -background Black
  Write-Host "Initial Password Length = $($PWD.length)".padright($padright) -ForegroundColor Green -background Black
  Write-Host "Final   Password Length = $($PWD.length)".padright($padright) -ForegroundColor Green -background Black
  Write-Host "".padright($padright,"-") -ForegroundColor Green -background Black
  Write-Host "Randomising Initial Password : $pwd ( Length = $($pwd.length) )".padright($padright) -ForegroundColor Yellow -background Black
 }
 #endregion Optionally Show password complexity information
 #region Randomise the password character order
 $sourcePWD = New-Object System.Collections.ArrayList($null)
 $sourcePWD.AddRange($pwd.ToCharArray())
 $newPWD = ''
 if ( ( $alphaFirst ) -and ( $numericFirst ) ) { Throw "ERROR: you cannot select both ALPHA and NUMERIC first switches" }
 if ( $alphaFirst ) {
  if ( $PWD -notmatch "[A-Za-z]" ) { Throw "Password MUST contain at least one ALPHA" }
 }
 if ( $numericFirst ) {
  if ( $PWD -notmatch "[0-9]" ) { Throw "Password MUST contain at least one NUMERAL" }
 }
 #for ($index = 0; $index -lt $PWD.Length; $index++) {
 $index = 0
 while ( $sourcePWD.length -gt 0 ) {
  #Write-Host "SOURCE : $($sourcePWD -join '') ($($sourcePWD.count))"
  #Write-Host "NEW PWD: $newPWD ($($newPWD.length))"
  try { $nextPWDChar = Get-Random -InputObject $sourcePWD }
  catch {
   $bp = 0
   Throw "FATAL ERROR: Complexity Rules have exhausted the character pool"
  }
  if ( ( $index -eq 0 ) -and ( $alphaFirst ) ) {
   while ($nextPWDChar  -notmatch "[a-z]") {
    $nextPWDChar = Get-Random -InputObject $sourcePWD
   }
  } # if ( $index -eq 0 ) -and ( $alphaFirst ) {
  elseIf ( ( $index -eq 0 ) -and ( $numericFirst ) ) {
   while ($nextPWDChar  -notmatch "[0-9]") {
    $nextPWDChar = Get-Random -InputObject $sourcePWD
   }
  } # if ( $index -eq 0 ) -and ( $alphaFirst ) {
  if ( $concurrentLimit -ne 0 ) {
   if ( $lastPWDChar -eq $nextPWDChar ) {
    if ( $concurrent.contains($nextPWDChar) ) {
     if ( $concurrent.item($nextPWDChar) -eq $concurrentLimit ) {
      Continue
     }
     else {
      $concurrent.item($nextPWDChar) = $concurrent.item($nextPWDChar) + 1
     }
    } # if ( $concurrent.contains($nextPWDChar) ) {
    else {
     $concurrent.Add($nextPWDChar,1)
    }
   } # if ( $lastPWDChar -eq $nextPWDChar ) {
  } # if ( $concurrentLimit -ne 0 ) {
	 $newPWD += $nextPWDChar
  $index ++
  $lastPWDChar = $nextPWDChar
  $sourcePWD.Remove($nextPWDChar)
 }
 if ( $showDebug ) {
  write-host "Password changed to          : $($newPWD) ( Length = $($newPWD.length) )".padright($padright) -ForegroundColor Yellow -background Black
  Write-Host "".padright($padright,"-") -ForegroundColor Green -background Black
 }
 #endregion Randomise the password character order
 Return  $newPWD
} # end Function Get-RandomPassword

Updating ARS scheduled task parameters with LDAPFilters in

Further to my post on doing this ( https://clan8blog.wordpress.com/2017/03/16/automating-ars-scheduled-task-parameters/) I realised that I’d left out one little trick, or two in getting and updating the scheduled task parameters:

  • The task parameters always get returned as strings so you need to convert these on the fly like this

if ( $debugLevel -eq ” ) { $defaultUsed = $true ; $debugLevel = 9 } else { $debugLevel = [int]$debugLevel }

  • A task parameter isn’t multivalued so you need to store these as comma separated strings and then convert in the script

if ( $emailAlert -eq ” ) { $defaultUsed = $true ; $emailAlert = $scriptOwner } else { $emailAlert = $emailAlert.split(“,”) }

Now you have another problem if you are using my method to upload any missing parameters as you can’t upload the array you just created! The solution is to convert it back to a comma separated string of course:  ’emailAlert’ = $($emailAlert -join “,”)

  • LDAPfilters will contain ampersands so you can’t store these in the scheduled task parameters, well you can but as with the email address example above, you need to do some conversions when you upload like this:   Convert the AMPERSANDS to “&amp;” like this  ‘ldapFilter’     = $ldapFilter.Replace(“&”,”&amp;”) and then upload

The reverse conversion happens automatically so no need to do a conversion.

Hope this helps

Converting between timeZones

Further to my post in enumerating the time zones using

[System.TimeZoneInfo]::GetSystemTimeZones() |
select standardName,daylightName,DisplayName

I wrote two functions to be able to determine the timezone of a user based on the city and country locations ( Get-TimeZone )  and one to convert a time between any two timezones.  As I’m UK based if I omit the fromTimeZone parameter my function defaults to GMT Standard Time.

Get-TimeZone needs a city, e.g. London and a Country Abbreviation, e.g. GB or US to return the timezone name that can then be used by the Convert_timeZone function.

The function listing also uses a technique I’m sure I’ve blogged about too that limits how many emails a function will send out.  It can be embarassing for a script to go wrong and send 1000’s of emails so you should set some expected limits that will trigger mechanisms to prevent yet more spam in your environment.

So that this function is stand alone I use script scope variables and just in case I forget to instantiate these before calling the function I use test-path to check if the variable already exists and create it if it doesn’t.

if ( ! ( Test-Path variable:script:maxEmailLimit ) ) {
 $script:maxEmailLimit = 10
}
  • maxEmailLimit is the maximum number of emails I’ll let my function send
  • emailSent tracks how many emails I sent and I compare it with my limit
  • MissingCity and Missing Country allow my main program to report these later

I use an embedded function Send-email to handle sending email alerts to myself so that I capture any missing City and Country locations. m The function increments the emailSent counter and also checks that my limit hasn’t been exceeded. If it has then I send a final warning email and then stop sending any more.

function Send-email {
 param ( $bodyText,$subject)
 $script:emailsSent ++
 if ( $script:emailsSent -gt $($script:maxEmailLimit+1) ) {
  return
 }
 elseif ( $script:emailsSent -eq $($script:maxEmailLimit+1) ) {
  Send-MailMessage `
   -To $scriptOwner `
   -From $scriptOwner `
   -SmtpServer "FQDNofMYSMTPServer" `
   -Body "TOO MANY EMAILS SENT please investigate`r`n $bodyText" `
   -Subject "TOO MANY EMAILS SENT- $subject"
 }
 else {
  Send-MailMessage `
   -To $scriptOwner `
   -From $scriptOwner `
   -SmtpServer "FQDNofMYSMTPServer" `
   -Body $bodyText `
   -Subject $subject
 }
}

To add a location you need to look up the timezone information using google and work out which timezone string to use

[System.TimeZoneInfo]::GetSystemTimeZones() |
select standardName,daylightName,DisplayName

Here’s the full listing of Get-TimeZone. It’s just a simple switch statement really.

function Get-TimeZoneID {
 param ( $country,$city )
 if ( ! ( Test-Path variable:script:maxEmailLimit ) ) {
  $script:maxEmailLimit = 10
 }
 if ( ! ( Test-Path variable:script:emailsSent ) ) {
  $script:emailsSent = 0
 }
 if ( ! ( Test-Path variable:script:MissingCity ) ) {
  $script:MissingCity = @()
 }
 if ( ! ( Test-Path variable:script:MissingCountry ) ) {
  $script:MissingCountry = @()
 }
 function Send-email {
  param ( $bodyText,$subject)
  $script:emailsSent ++
  if ( $script:emailsSent -gt $($script:maxEmailLimit+1) ) {
   return
  }
  elseif ( $script:emailsSent -eq $($script:maxEmailLimit+1) ) {
   Send-MailMessage -To $scriptOwner -From $scriptOwner -SmtpServer "FQDNofMYSMTPServer" -Body "TOO MANY EMAILS SENT please investigate`r`n $bodyText" -Subject "TOO MANY EMAILS SENT- $subject"
  }
  else {
   Send-MailMessage -To $scriptOwner -From $scriptOwner -SmtpServer "FQDNofMYSMTPServer" -Body $bodyText -Subject $subject
  }
 }
 switch ( $country ) {
  "AU"                   { $timeZoneID = "AUS Eastern Standard Time"      ; break } # Australia
  "CA"                   { $timeZoneID = "Eastern Standard Time"          ; break } # Canada
  "DE"                   { $timeZoneID = "Central European Standard Time" ; break } # Germany
  "GB"                   { $timeZoneID = "GMT Standard Time"              ; break } # United Kingdom
  "US" {
   switch ( $city ) {
    "Chicago"       { $timeZoneID = "Central Standard Time"    ; break }
    "Los Angeles"   { $timeZoneID = "Pacific Standard Time"    ; break }
    "New York"      { $timeZoneID = "US Eastern Standard Time" ; break }
    "Off-site"      { $timeZoneID = "US Eastern Standard Time" ; break } # Off Site at a specific location
    default         {
     $timeZoneID = "US Eastern Standard Time"
     if ( $script:MissingCity -notcontains $city ) {
      $script:missingCity += $city
      Send-email -bodyText  "Missing TZ for US CITY:'$city'" -subject "Missing TZ in $scriptName ARS Script Version: $($scriptVersion)"
     }
    }
   }
   break
  }
  default                {
   $timeZoneID = "GMT Standard Time"
   if ( $script:MissingCcountry -notcontains $country ) {
    $script:missingCountry += $country
    Send-email -bodyText  "Missing TZ for CITY:'$city' in COUNTRY:'$country'" -subject  "Missing TZ in $scriptName ARS Script Version: $($scriptVersion)"
   }
  }
 }
 return $timeZoneID
}

Convert-TimeZone is a little more complex and I plagiarized most of this from google.

To convert to another timezone from GMT use this command line.

$expiryTime = get-date $(Convert-TimeZone -time $user.PasswordExpires -fromTimeZoneID “GMT Standard Time” -toTimeZoneID $(Get-TimeZoneID -city $user.l -country $user.c))

Where $user is an AD user with attributes l = city and c = country abbr

function Convert-TimeZone {
 [CmdletBinding()]
 param(
  [Parameter(Mandatory=$false)]
   $time,
   [Parameter(Mandatory=$false)]
   $fromTimeZoneID,
   [Parameter(Mandatory=$false)]
   $toTimeZoneID
 )
 # if no from timezone provided use the local timezone
 if ( ! ( $fromTimeZoneID ) ) {
  #$fromTimeZoneID = (([System.TimeZoneInfo]::Local).Id).ToString()
  $fromTimeZoneID = "GMT Standard Time"
 }
 if ( $fromTimeZoneID -eq $toTimeZoneID ) {
  # nothing to do other than convert the date to a string and return
  return  $(Get-Date($time) -format "dddd, MMMM dd, yyyy h:mm:ss tt")
 }
 # if no time given as a parameter use current date and time using get-date
 if (!($time))   {
  $time = [DateTime]::SpecifyKind((Get-Date), [DateTimeKind]::Unspecified)
 }
 else {
  try { 
   $time = [DateTime]::SpecifyKind((Get-Date([dateTime]$time)), 
           [DateTimeKind]::Unspecified) }
  catch {
   $breakPoint = 0
  }
 }
 $Error.Clear()
 try { 
  $UTC = [System.TimeZoneInfo]::ConvertTimeToUtc($time,
         [System.TimeZoneInfo]::FindSystemTimeZoneById($fromTimeZoneID)) 
 }
 catch {
  $Error
 }
 if ($toTimeZoneID) {
  try { $toTimeZoneTime = [System.TimeZoneInfo]::ConvertTime($UTC, 
        [System.TimeZoneInfo]::FindSystemTimeZoneById($toTimeZoneID)) 
  }
  catch {
   $Error
  }
  Write-Verbose ("{0}: {1}" -f $fromTimeZoneID, $time)
  Write-Verbose ("{0}: {1}" -f $toTimeZoneID, $toTimeZoneTime)
 } 
 else {
  Write-Verbose ("{0}: {1}" -f $fromTimeZoneID, $time)
  $times = @()
  foreach ($timeZone in ([system.timezoneinfo]::GetSystemTimeZones())) {
   $times += (New-Object psobject -Property @{'Name' = $timeZone.DisplayName;
   'ID' = $timeZone.id; 'Time' = 
   [System.TimeZoneInfo]::ConvertTime($UTC,`
   System.TimeZoneInfo]::FindSystemTimeZoneById($timeZone.id)); `
   'DST' = $timeZone.SupportsDaylightSavingTime})
  }
  $times | Sort-Object Time | Format-Table -Property * -AutoSize
 }
 try {
  $toTimeZoneTime = $(Get-Date($toTimeZoneTime) `
                         -format "dddd, MMMM dd, yyyy h:mm:ss tt")
 }
 catch {
  $toTimeZoneTime = $null
 }
  return $toTimeZoneTime
}

How to get your mugshot photo into AD

My previous posts on this topic explained how to convert the format to a  jpeg  then resize it to fit into the AD size limit and then finally how to upload it into AD in the thumbnailAttribute.

This worked fine for years but recently we had some complaints that the pictures were not consistent. AD , SharePoint and Outlook were not all showing the same photograph.

The long and the short of this is that exchange creates it’s own copy on the exchange server and if you don’t use the exchange commandlet ‘Set-UserPhoto’ to upload the photo then this cache may not be updated.  You can force it to be updated by deleting the cached photo but it’s better to just use the new commandlet like this:

Set-UserPhoto -Identity $guid -PictureData ([System.IO.File]::ReadAllBytes($jpegFile)) -Confirm:$false