Converting Password Last Set to Daylight saving

The password last set attribute pwdLastSet is actually stored as a ‘large integer’ which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC).   You can find a full explanation on the Microsoft doc web page https://docs.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset

It’s easy to get this value converted to a [datetime] object using powershells Get-Date commandlet but what you may not have noticed is that this time is in UTC and if you probably need it to display in local time, including Daylight Saving.  All you need to do is is use the ‘AddHours’ method of a [dateTime] object and add an hour to the time.  The problem is that we are not always in Daylight saving so how do we figure that out.  Fortunately lots of people have already posted how to determine if you are in Daylight Saving or not.  Here is the Microsoft link https://gallery.technet.microsoft.com/Get-DSTInfo-Determine-Time-8f7f9f91 to a Get-DSTInfo function.

Here is an example way of using this function.

Set a variable using the Get-DSTInfo function like this $dayLightSaving = if ( (get-date) -ge (Get-DSTInfo).DaylightChgDate ) { 1 } else { 0 } now we have a variable that it set to 1 or 0 depending on if we are in DST or not.  Now when we get the user objects from AD we can simply create a custom attribute like this:

Get-ADuser <userName> -properties pwdLastSet | select-object sAMAccountName,,@{n=’Password Last Set’;e={ get-date -Date (Get-Date $_.pwdLastSet).AddHours($dayLightSaving)}}

 

 

Updating the ARS Password generator part 2

There is an existing password generator located here: Configuration/Script Modules/Builtin/Generate User Password – PowerShell and this is linked to the domin using an ARS script policy located here : Configuration/Policies/Administration/Builtin/Built-in Policy – Password Generation

The script consists of the following functions:

  • function onInit($Context) – used to configure the parameters that can be set in the ARS script policy object
  • function onGetEffectivePolicy($Request) – this is the function that reacts to the ‘Generate Password’ button when resetting a users password and calls the function New-Password that will return the new password
  • function New-Password – this is function we will substitute with the pass phrase generator I published in my last post

Step one then is to copy the script and create a new one that you can edit.

If we look at the comments in the original onGetEffectivePolicy function we can see it does the following:

  1. # Mark password as server-side generated.
  2. # Determine whether server-side generation is requested (button pressed)
  3. # Get domain minimum password length value.
  4. # Check for fine grained password policy for the user
  5. # Set minimum password length.
  6. # Get user name information; first name, last name & sAMAccountName
  7. # Populate new password parameter values from policy parameters
  8. # Calculate most restrictive minimum password length to be used
  9. # Get character input strings from policy.
  10. # Add special characters to input strings if enabled in policy
  11. # Generate password and check that user’s names are not contained within the password
  12. # Set password value in form

I’ve highlighted the sections we can reuse with some modifications.

For debug purposes I added a function that writes to the ARS event log:

function Write-ARSEventLog([int]$verbosity, [string]$str ) {
# outputs debug info to the EDM event log
 if ( [string]$PolicyEntry.Parameter("debugging") -ne '0' ) {
  $strDebuggingSwitch = [string]$PolicyEntry.Parameter("debugging")
  if ( $verbosity -le [int]$strDebuggingSwitch ) {
   $EventLog.ReportEvent(2,$str)
  } 
 }
} # Write-ARSEventLog

I then replaced the New-Password Function with my New-Passphrase function and then modified the onGetEffectivePolicy function to call my function instead of the New-Password function. You can see that I call my new Write-ARSEventLog function but only if the debugLevel is Greater then 7. Writing to the log is going to slow down the script a little but if it’s not working you’ll find this invaluable. Don’t forget to also add the New-RandomNumber function that is called by the New-Passphrase function.

This is the line that calls the New-passphrase function:
$Password = New-PassPhrase -WordCount $([string]$PolicyEntry.Parameter(“MimimimPassphraseWords”)) -passphraseLength $([string]$PolicyEntry.Parameter(“MinimumPasswordLength”)) -special $([string]$PolicyEntry.Parameter(“SPECIAL”)) -numeric $([string]$PolicyEntry.Parameter(“NUMERIC”)) `
-lower $([string]$PolicyEntry.Parameter(“LOWERCASE”)) -upper $([string]$PolicyEntry.Parameter(“UPPERCASE”)) -WordListLONGLiteralPath $([string]$PolicyEntry.Parameter(“LONGWORDLISTPATH”)) -WordListSHORTLiteralPath $([string]$PolicyEntry.Parameter(“SHORTWORDLISTPATH”))

The function parameters are set using the ARS script policy parameters which are set in the onInit function. You can get the parameter value using $PolicyEntry.Parameter(“PARAMETERNAME”)). I could have hard coded all the parameters but this way I can create multiple ARS policies that are applied to different OUs or MUs ( Managed Units ) and reuse the same script for each. It also means you can change the password complexity very easily by editing the parameters of the ARS script policy.

function onGetEffectivePolicy($Request) {
 $errcount = $Error.count
 $context.UseLibraryScript("PowerShell Best Practices")
 if (($Request.Class -ne "inetOrgPerson") -AND ($Request.Class -ne "user") ) {$Request.ReportEvent($Constants.EDS_EVENTLOG_WARNING_TYPE, "CustomPasswordPolicy-Wrong object type-exit"); return } # original line from script
 #if (($Request.Class -ne "inetOrgPerson") -AND ($Request.Class -ne "user") ) { $Request.ReportEvent(2, "CustomPasswordPolicy-Wrong object type-exit"); return } # original line from script
 # Mark password as server-side generated.
 $Request.SetEffectivePolicyInfo("edsaPassword",$Constants.EDS_EPI_UI_SERVER_SIDE_GENERATED, $false) # original line from script
 #$Request.SetEffectivePolicyInfo("edsaPassword",$Constants.EDS_EPI_UI_SERVER_SIDE_GENERATED, $true)
 $Request.SetEffectivePolicyInfo("edsaPassword",12, $true)
 # Determine whether server-side generation is requested (button pressed).
 $controlFullPolicyInfo = $Request.GetInControl($constants.EDS_CONTROL_FULL_EFFECTIVE_POLICY_INFO) # original line from script
 #$controlFullPolicyInfo = $Request.GetInControl(31) 
 if ($Error.count –ne $errcount) { $controlFullPolicyInfo = "" }
 if ($controlFullPolicyInfo -ne "edsaPassword") { return }
 Write-ARSEventLog -verbosity 8 -str "Set-SecurePassphrase onGetEffectivePolicy &gt;&gt;&gt; REQUESTING '$([string]$PolicyEntry.Parameter("MimimimPassphraseWords"))' WORDS : REQUESTING MINIMUM LENGTH'$([string]$PolicyEntry.Parameter("MinimumPasswordLength"))'"
 $Password = New-PassPhrase -WordCount $([string]$PolicyEntry.Parameter("MimimimPassphraseWords"))  -passphraseLength $([string]$PolicyEntry.Parameter("MinimumPasswordLength")) -special $([string]$PolicyEntry.Parameter("SPECIAL")) -numeric $([string]$PolicyEntry.Parameter("NUMERIC")) -lower $([string]$PolicyEntry.Parameter("LOWERCASE")) -upper $([string]$PolicyEntry.Parameter("UPPERCASE")) -WordListLONGLiteralPath $([string]$PolicyEntry.Parameter("LONGWORDLISTPATH")) -WordListSHORTLiteralPath $([string]$PolicyEntry.Parameter("SHORTWORDLISTPATH"))
 Write-ARSEventLog -verbosity 9 -str "Set-SecurePassphrase onGetEffectivePolicy &gt;&gt;&gt; PASSPHRASE = $Password" 
 $Request.SetEffectivePolicyInfo("edsaPassword",$Constants.EDS_EPI_UI_GENERATED_VALUE, $Password) # original line from script
 #$Request.SetEffectivePolicyInfo("edsaPassword",7, $Password) 
 $Request.SetEffectivePolicyInfo("edsaPassword","7", $Password)
}

Almost done next we add a function that is supposed to replace the builtin password policy.  I’ve included it here but I was unable to get it to work and Quest will not support custom scripts that you write.  Strangely when pressing the password generate button it would randomly choose between my new script and the built in script.  I have no idea why this does not work and instead I just unlinked the built in policy from the Active Directory Node.  If you do this then you need to make sure an ARS password policy is linked to every OU where you want it applied.  I have several with different complexities set, e.g. for personal employee accounts  and personal functional accounts I use pass phrases of different lengths and for service accounts I use just random passwords of 20 characters or more, after all these will not be used interactively and most likely never reset to need to be long and strong.

function onGetPolicyMarker() {
 # Override built-in Generate User Password Policy
 return "Generate User Password - powershell"
} 

I then updated the onInit function to create all the variable parameters I need to call the New-Passphrase function

Parameters 7 and 8 define the location of the WORDLISTs stored locally on ALL of your ARS servers

$par07 = $context.AddParameter(“LONGWORDLISTPATH”)
$par07.MultiValued = $false
$par07.DefaultValue = ‘C:\temp\wordlist.csv’
$par07.Description = “Path to wordlist used to generate passphrase”
$par07.Required = $true
$par08 = $context.AddParameter(“SHORTWORDLISTPATH”)
$par08.MultiValued = $false
$par08.DefaultValue = ‘C:\temp\wordlistSHORT.csv’
$par08.Description = “Path to wordlist used to generate passphrase”
$par08.Required = $true

function onInit($Context) {
 $par01 = $context.AddParameter("MinimumPasswordLength")
 $par01.MultiValued = $false
 $par01.PossibleValues = "8","9","10","11","12","13","14","15","16","17","18","19","20"
 $par01.DefaultValue = "12"
 $par01.Description = "Minimum Passphrase Length Generated when clicking the Auto Generate Password button when resetting a user password"
 $par01.Required = $true
 $par02 = $context.AddParameter("MimimimPassphraseWords")
 $par02.MultiValued = $false
 $par02.PossibleValues = "1","2","3","4","5","6","7","8"
 $par02.DefaultValue = "3"
 $par02.Description = "Minimum number of words to create in a Passphrase"
 $par02.Required = $true
 $par03 = $context.AddParameter("SPECIAL")
 $par03.MultiValued = $false
 $par03.PossibleValues = "-1","0","1"
 $par03.DefaultValue = "1"
 $par03.Description = "If set to 1 passphrase will include 1 or more SPECIAL characters, set to 0 prevents SPECIAL characters being generated"
 $par03.Required = $true
 $par04 = $context.AddParameter("NUMERIC")
 $par04.MultiValued = $false
 $par04.PossibleValues = "-1","0","1"
 $par04.DefaultValue = "1"
 $par04.Description = "If set to 1 passphrase will include 1 or more NUMERIC characters, set to 0 prevents NUMERIC characters being generated"
 $par04.Required = $true
 $par05 = $context.AddParameter("UPPERCASE")
 $par05.MultiValued = $false
 $par05.PossibleValues = "-1","0","1"
 $par05.DefaultValue = "1"
 $par05.Description = "If set to 1 passphrase will include 1 or more UPPERCASE characters, set to 0 prevents UPPERCASE characters being generated"
 $par05.Required = $true
 $par06 = $context.AddParameter("LOWERCASE")
 $par06.MultiValued = $false
 $par06.PossibleValues = "-1","0","1"
 $par06.DefaultValue = "1"
 $par06.Description = "If set to 1 passphrase will include 1 or more LOWERCASE characters, set to 0 prevents LOWERCASE characters being generated"
 $par06.Required = $true
 $par07 = $context.AddParameter("LONGWORDLISTPATH")
 $par07.MultiValued = $false
 $par07.DefaultValue = 'C:\temp\wordlist.csv'
 $par07.Description = "Path to wordlist used to generate passphrase"
 $par07.Required = $true
 $par08 = $context.AddParameter("SHORTWORDLISTPATH")
 $par08.MultiValued = $false
 $par08.DefaultValue = 'C:\temp\wordlistSHORT.csv'
 $par08.Description = "Path to wordlist used to generate passphrase"
 $par08.Required = $true
 $par09 = $context.AddParameter("debugging")
 $par09.MultiValued = $false
 $par09.PossibleValues = "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
 $par09.DefaultValue = "0"
 $par09.Description = "Debugging EventLog Level where: 0 is no debugging; 9 is the most verbose; 1 is the least verbose"
 $par09.Required = $false
}

Now we are ready to test it. Create a new ARS script policy and add the script to it like this and then set the parameters as required to implement your password policy.

-1 means the script can randomly decide to use or not the character set.  In this example the passphrase will always include UPPER and lowercase characters but may also include special and numeric characters.  The three possible values that can be set are:

  • -1 may include
  • 0 MUST NOT include
  • 1 MUST  include

Now we link that policy to the OU containing the users we want that policy to apply to and we can finally start testing those new passphrases ( which is probably more fun than it should be ).