A single, self-extracting, self-scheduling, AD password change notice PowerShell script.
One of the great things about sharing PowerShell code is that it can be expanded upon by the community. Here is a case where I’ve taken someone else’s great work and made it (hopefully) a little bit better.
Description
This script notifies users that their password is about to expire. I’ve taken the work done by others and added some features to make it easier to schedule and manage.
The overall changes to the original script were meant to facilitate multiple runs of the script to send out notifications in a less spammy way. For example:
- 7 Days until the password will expire -> Send a warning (yellow) email notice
- 3 Days until the password will expire -> Send an alert (red) email notice
- 1 Day until the password will expire -> Send a final alert (red) notice
I’ve also made numerous readability and other feature additions. This includes (but is not limited to);
- Ability to distribute the script in 1 file and self-extract the gif files used in the notices.
- Total rewrite of the notice generation to be a bit more centralized (and therefore easier to locate and customize if needed)
- Elimination of global variables
- Conversion of all local variables into parameters
- Self-referencing scheduled task installation routine (schedules the task with the same parameters the script itself was passed)
- A ‘LooseMatching’ mode to facilitate exact matching.
If you want the dummy way of running this script simply copy to your server and run the following command:
Show-Command .\New-ADPasswordReminder.ps1
You then get a nice little GUI like this:
The examples below should be sufficient to get you started with this script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<# .EXAMPLE .\New-ADPasswordReminder.ps1 -ExtractGifs Description -------------- Extract the embedded gifs used in the notifications generated by this script. These should be then moved over to a publicly available web server. .EXAMPLE .\New-ADPasswordReminder.ps1 -Demo -DaysToWarn 5 Description -------------- Searches Active Directory for users who have passwords expiring in 5 days, and lists those users on the screen, along with days till expiration and policy setting .EXAMPLE .\New-ADPasswordReminder.ps1 -DaysToWarn 7 -Demo -LooseMatching Description -------------- Query AD for any accounts with passwords that will expire in 7 days or less and print them to the screen. .EXAMPLE .\New-ADPasswordReminder.ps1 -DaysToWarn 7 -Demo Description -------------- Query AD for any accounts with passwords that will expire in EXACTLY 7 days and print them to the screen. .EXAMPLE .\New-ADPasswordReminder.ps1 -Alert -PreviewUser 'jdoe' -Company 'Contoso' -PasswordChangeURL 'https://sso.contoso.com/adfs/portal/updatepassword/' -EmailServer 'smtprelay.contoso.com' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath 'https://www.contoso.com/notices' -Install Description -------------- Create a new scheduled task test for the jdoe user. This will automatically assume that jdoe's password changes in a day and send a red (alert) email notification. .EXAMPLE $MyCred = Get-Credential .\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL 'https://sso.contoso.com/adfs/portal/updatepassword/' -EmailServer 'smtprelay.contoso.com' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath 'https://www.contoso.com/notices' -Install -DaysToWarn 7 -Credential $MyCred .\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL 'https://sso.contoso.com/adfs/portal/updatepassword/' -EmailServer 'smtprelay.contoso.com' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath 'https://www.contoso.com/notices' -Install -DaysToWarn 3 -Alert -Credential $MyCred .\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL 'https://sso.contoso.com/adfs/portal/updatepassword/' -EmailServer 'smtprelay.contoso.com' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath 'https://www.contoso.com/notices' -Install -DaysToWarn 1 -Alert -Credential $MyCred Description -------------- Create a new scheduled task that will send a warning notice on a daily basis to users that have passwords that will expire in 7 days then again in 3 days. Also, create another similar task that will send an alert to users that have one more day to change their passwords. #> |
Running as a Managed Service Account
In newer versions of Windows AD you can create managed service accounts which reduce yet another account password you have to keep note of in your environment. I’ve tested this script using an MSA and it seems to work just fine. Here are the steps you’d need to follow to update the scheduled tasks created by this script to use an MSAs instead.
Firstly you will need to setup your MSA. Replace anything in angle brackets as needed (Computer = the server you will be scheduling the tasks on):
1 2 3 4 5 |
New-ADServiceAccount -SamAccountName "ADPWNotice" -Name "ADPWNotice" -Description "Account used for running the AD PW notification task on <Computer>" -DNSHostName <Domain Controller> Set-ADServiceAccount -Identity ADPWNotice -PrincipalsAllowedToRetrieveManagedPassword <Computer>$ -Enabled $true Add-ADComputerServiceAccount -Identity "<Domain Controller>" -ServiceAccount "ADPWNotice" Add-ADGroupMember "Domain Users" "CN=ADPWNotice,CN=Managed Service Accounts,DC=<contoso>,DC=<com>" Add-ADGroupMember "Backup Operators" "CN=ADPWNotice,CN=Managed Service Accounts,DC=<contoso>,DC=<com>" |
Next create your scheduled tasks like you normally would. When prompted for a user id/password put in your administrative account. This will only be a temporary assignment until you update with the MSA.
This example will schedule a password change notification 7 days before it needs to be changed.
1 |
.\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL 'https://sso.contoso.com/adfs/portal/updatepassword/' -EmailServer 'smtprelay.contoso.com' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath 'https://www.contoso.com/notices' -Install -DaysToWarn 7 |
You will need to update the computer where the scheduled tasks will run to allow the MSA some local rights. Here is a handy script to do just that (notice the MSA includes the $ at the end).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
Function Add-UserToLocalSecurityRight { <# .SYNOPSIS When run administratively this will add a user to the local system's login local rights security policy. .DESCRIPTION When run administratively this will add a user to the local system's login local rights security policy. .PARAMETER UserID User ID to add to the local system's login local rights security policy. .PARAMETER LocalRight Local right to grant. Either 'LogonAsBatch' or 'LogonLocal' .LINK http://www.the-little-things.net .NOTES Version: 1.0.0 - Initial release 1.0.1 - Updated to include logon local right and parameter to select the right to assign. Author: Zachary Loeber Respect: Code mildy modified from http://www.morgantechspace.com/2014/03/Set-Logon-as-batch-job-rights-to-User-by-Powershell-CSharp-CMD.html .EXAMPLE Add-UserToLoginAsBatch 'test.user' -LogonRight 'LogonAsBatch' Description ----------- Adds the local user test.user to the login as batch job rights on the local machine. #> [CmdletBinding()] param( [parameter()] [string]$UserID, [parameter()] [ValidateSet('LogonAsBatch','LogonLocal')] [string]$LocalRight = 'LogonAsBatch' ) $CSharpCode = @' using System; // using System.Globalization; using System.Text; using System.Runtime.InteropServices; public class LsaWrapper { // Import the LSA functions [DllImport("advapi32.dll", PreserveSig = true)] private static extern UInt32 LsaOpenPolicy( ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, Int32 DesiredAccess, out IntPtr PolicyHandle ); [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern long LsaAddAccountRights( IntPtr PolicyHandle, IntPtr AccountSid, LSA_UNICODE_STRING[] UserRights, long CountOfRights); [DllImport("advapi32")] public static extern void FreeSid(IntPtr pSid); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)] private static extern bool LookupAccountName( string lpSystemName, string lpAccountName, IntPtr psid, ref int cbsid, StringBuilder domainName, ref int cbdomainLength, ref int use); [DllImport("advapi32.dll")] private static extern bool IsValidSid(IntPtr pSid); [DllImport("advapi32.dll")] private static extern long LsaClose(IntPtr ObjectHandle); [DllImport("kernel32.dll")] private static extern int GetLastError(); [DllImport("advapi32.dll")] private static extern long LsaNtStatusToWinError(long status); // define the structures private enum LSA_AccessPolicy : long { POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L, POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L, POLICY_GET_PRIVATE_INFORMATION = 0x00000004L, POLICY_TRUST_ADMIN = 0x00000008L, POLICY_CREATE_ACCOUNT = 0x00000010L, POLICY_CREATE_SECRET = 0x00000020L, POLICY_CREATE_PRIVILEGE = 0x00000040L, POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L, POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L, POLICY_AUDIT_LOG_ADMIN = 0x00000200L, POLICY_SERVER_ADMIN = 0x00000400L, POLICY_LOOKUP_NAMES = 0x00000800L, POLICY_NOTIFICATION = 0x00001000L } [StructLayout(LayoutKind.Sequential)] private struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public readonly LSA_UNICODE_STRING ObjectName; public UInt32 Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; } [StructLayout(LayoutKind.Sequential)] private struct LSA_UNICODE_STRING { public UInt16 Length; public UInt16 MaximumLength; public IntPtr Buffer; } /// //Adds a privilege to an account /// Name of an account - "domain\account" or only "account" /// Name of the privilege /// The windows error code returned by LsaAddAccountRights public long SetRight(String accountName, String privilegeName) { long winErrorCode = 0; //contains the last error //pointer an size for the SID IntPtr sid = IntPtr.Zero; int sidSize = 0; //StringBuilder and size for the domain name var domainName = new StringBuilder(); int nameSize = 0; //account-type variable for lookup int accountType = 0; //get required buffer size LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); //allocate buffers domainName = new StringBuilder(nameSize); sid = Marshal.AllocHGlobal(sidSize); //lookup the SID for the account bool result = LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); //say what you're doing Console.WriteLine("LookupAccountName result = " + result); Console.WriteLine("IsValidSid: " + IsValidSid(sid)); Console.WriteLine("LookupAccountName domainName: " + domainName); if (!result) { winErrorCode = GetLastError(); Console.WriteLine("LookupAccountName failed: " + winErrorCode); } else { //initialize an empty unicode-string var systemName = new LSA_UNICODE_STRING(); //combine all policies var access = (int) ( LSA_AccessPolicy.POLICY_AUDIT_LOG_ADMIN | LSA_AccessPolicy.POLICY_CREATE_ACCOUNT | LSA_AccessPolicy.POLICY_CREATE_PRIVILEGE | LSA_AccessPolicy.POLICY_CREATE_SECRET | LSA_AccessPolicy.POLICY_GET_PRIVATE_INFORMATION | LSA_AccessPolicy.POLICY_LOOKUP_NAMES | LSA_AccessPolicy.POLICY_NOTIFICATION | LSA_AccessPolicy.POLICY_SERVER_ADMIN | LSA_AccessPolicy.POLICY_SET_AUDIT_REQUIREMENTS | LSA_AccessPolicy.POLICY_SET_DEFAULT_QUOTA_LIMITS | LSA_AccessPolicy.POLICY_TRUST_ADMIN | LSA_AccessPolicy.POLICY_VIEW_AUDIT_INFORMATION | LSA_AccessPolicy.POLICY_VIEW_LOCAL_INFORMATION ); //initialize a pointer for the policy handle IntPtr policyHandle = IntPtr.Zero; //these attributes are not used, but LsaOpenPolicy wants them to exists var ObjectAttributes = new LSA_OBJECT_ATTRIBUTES(); ObjectAttributes.Length = 0; ObjectAttributes.RootDirectory = IntPtr.Zero; ObjectAttributes.Attributes = 0; ObjectAttributes.SecurityDescriptor = IntPtr.Zero; ObjectAttributes.SecurityQualityOfService = IntPtr.Zero; //get a policy handle uint resultPolicy = LsaOpenPolicy(ref systemName, ref ObjectAttributes, access, out policyHandle); winErrorCode = LsaNtStatusToWinError(resultPolicy); if (winErrorCode != 0) { Console.WriteLine("OpenPolicy failed: " + winErrorCode); } else { //Now that we have the SID an the policy, //we can add rights to the account. //initialize an unicode-string for the privilege name var userRights = new LSA_UNICODE_STRING[1]; userRights[0] = new LSA_UNICODE_STRING(); userRights[0].Buffer = Marshal.StringToHGlobalUni(privilegeName); userRights[0].Length = (UInt16) (privilegeName.Length*UnicodeEncoding.CharSize); userRights[0].MaximumLength = (UInt16) ((privilegeName.Length + 1)*UnicodeEncoding.CharSize); //add the right to the account long res = LsaAddAccountRights(policyHandle, sid, userRights, 1); winErrorCode = LsaNtStatusToWinError(res); if (winErrorCode != 0) { Console.WriteLine("LsaAddAccountRights failed: " + winErrorCode); } LsaClose(policyHandle); } FreeSid(sid); } return winErrorCode; } } public class AddUserLocalRight { public static void GrantUserLogonAsBatchJob(string userName) { try { LsaWrapper lsaUtility = new LsaWrapper(); lsaUtility.SetRight(userName, "SeBatchLogonRight"); Console.WriteLine("Logon as batch job right is granted successfully to " + userName); } catch (Exception ex) { Console.WriteLine(ex.Message); } } public static void GrantUserLogonLocal(string userName) { try { LsaWrapper lsaUtility = new LsaWrapper(); lsaUtility.SetRight(userName, "SeInteractiveLogonRight"); Console.WriteLine("Logon local right is granted successfully to " + userName); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } '@ try { Add-Type -ErrorAction Stop -Language:CSharpVersion3 -TypeDefinition $CSharpCode } catch { Write-Error $_.Exception.Message break } if ($LocalRight -eq 'LogonAsBatch') { [AddUserLocalRight]::GrantUserLogonAsBatchJob($UserID) } if ($LocalRight -eq 'LogonLocal') { [AddUserLocalRight]::GrantUserLogonLocal($UserID) } } Add-UserToLocalSecurityRight -UserID "<Domain>\ADPWNotice$" -LocalRight 'LogonAsBatch' Add-UserToLoginAsBatch "<Domain>\ADPWNotice$" -LocalRight 'LogonLocal' |
Now that you have your starting scheduled task you will need to update it to use the MSA. In this example I create a new scheduled task like the existing one but using the MSA instead.
1 2 3 4 |
$oldtask = Get-ScheduledTask "AD Password Expiration Notification (7 Day Warning)" $trigger = New-ScheduledTaskTrigger -At 12:00 -Daily $principal = New-ScheduledTaskPrincipal -UserID "<Domain>\ADPWNotice$" -LogonType Password Register-ScheduledTask "AD Password Expiration Notification (7 Day Warning) - MSA" –Action $oldtask.Actions[0] –Trigger $trigger –Principal $principal |
I think you can just update the task with the MSA and an empty password as well:
1 |
schtasks /Change /TN "AD Password Expiration Notification (7 Day Alert)" /RU "ADPWNotice$" /RP "" |
And that’s all there is to it really. The scheduled task will run if all the local rights are set and you prayed to the correct IT gods and the phase of the moon is just right.
Other Information
Github Project Site: https://github.com/zloeber/New-ADPasswordReminder
Original Script Site: http://www.ehloworld.com/318
12:11 PM, 08/07/2017Joe /
I’m getting:
-replace : The term ‘-replace’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again
It does send the email out anyways. It seems as though it an issue with the password complexity blurp within the email.
9:03 PM, 08/08/2017Zachary Loeber /
Which line throws that error?
5:38 AM, 08/09/2017Joe /
I figured it out. I had a formatting error and it did not copy and paste correctly. An excellent script by the way I really appreciate you sharing it.
9:06 PM, 08/10/2017Zachary Loeber /
I’m glad you like it. If you make any improvements please pass them along and I’ll try to get them integrated 🙂