{"id":54873,"date":"2008-11-18T11:23:00","date_gmt":"2008-11-18T11:23:00","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2008\/11\/18\/hey-scripting-guy-how-can-i-find-expiring-passwords\/"},"modified":"2008-11-18T11:23:00","modified_gmt":"2008-11-18T11:23:00","slug":"hey-scripting-guy-how-can-i-find-expiring-passwords","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/hey-scripting-guy-how-can-i-find-expiring-passwords\/","title":{"rendered":"Hey, Scripting Guy! How Can I Find Expiring Passwords?"},"content":{"rendered":"<h2><img decoding=\"async\" class=\"nearGraphic\" title=\"Hey, Scripting Guy! Question\" height=\"34\" alt=\"Hey, Scripting Guy! Question\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/q-for-powertip.jpg\" width=\"34\" align=\"left\" border=\"0\" \/> <\/h2>\n<p>Hey, Scripting Guy! The pointy headed boss (PHB) is at it again. He has decided he does not like it when users do not change their password when prompted. As a result, he wants me to produce a report once a week of all the users who will have to change their password next week. Please tell me it can\u2019t be done so I can get him off my back. I looked in Active Directory Domain Services Users and Computers, but I do not see this listed anywhere.<\/p>\n<p>&#8211; AM<\/p>\n<p><img decoding=\"async\" height=\"5\" alt=\"Spacer\" src=\"https:\/\/devblogs.microsoft.com\/scripting\/wp-content\/uploads\/sites\/29\/2019\/05\/spacer.gif\" width=\"5\" border=\"0\" \/><img decoding=\"async\" class=\"nearGraphic\" title=\"Hey, Scripting Guy! Answer\" height=\"34\" alt=\"Hey, Scripting Guy! Answer\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/a-for-powertip.jpg\" width=\"34\" align=\"left\" border=\"0\" \/> <\/p>\n<p>Hi AM,<\/p>\n<p>Sometimes it is just better to go with the flow. The astute network administrator can always make things seem to work out for the best when confronted with a PHB. I dare not ask if he wants you to personally call each user and help them in picking out good passwords, and stand by just in case they get it wrong or lock themselves out. Because everyone knows that network administrators really do not do all that much anyway (according to PHB-think), maybe you should suggest this to your boss. Maybe the entire IT department can take each Tuesday\u2014instead of doing useless things like testing patches\u2014to help the users with their passwords. Or maybe not! Sorry, having been there, done that, and worn the scars, I am still a bit sensitive. Why do you think I run for an hour each day? Here is a better suggestion, which I have made before and will make again. Tell the PHB it will take you two days to retrieve the results. Run this script, and then go scuba diving. This view should give you some incentive:<\/p>\n<p><img decoding=\"async\" height=\"338\" alt=\"Image of underwater gorgeousness\" src=\"http:\/\/img.microsoft.com\/library\/media\/1033\/technet\/images\/scriptcenter\/qanda\/hsg\/hey1118\/hsg_password1.jpg\" width=\"450\" border=\"0\" \/> <\/p>\n<p>&nbsp;<\/p>\n<p>Here is the <b>ExpiringUserPasswords.ps1<\/b> script. If you want to leave now and get some dives in before dinner, we&#8217;ll&nbsp;understand:<\/p>\n<pre class=\"codeSample\">$MaxPasswordAge = 30\n$userCount = 0\n$adsiSearcher = new-object DirectoryServices.DirectorySearcher(\"LDAP:\/\/rootdse\")\n$adsiSearcher.filter = \"objectCategory=user\"\n$adsiSearcher.findall() | \nForeach-Object -ErrorAction \"silentlycontinue\" `\n-Begin { \"The following users need to set their  password\" } `\n-Process `\n{ \n $pwdChanged = ([adsi]$_.path).psbase.InvokeGet(\"PasswordLastChanged\")\n If( ((get-date) - $pwdChanged).days -ge $MaxPasswordAge)\n   { \n    ([adsi]$_.path).name \n    $userCount ++\n   } #end if date\n} `\n-end { \"A total of $userCount users\" }\n<\/pre>\n<p>The first thing we do in the <b>ExpiringUserPasswords.ps1<\/b> script is create a couple of variables. The first variable is <b>$MaxPasswordAge<\/b> which we set to 30. You would set this value to whatever your password age policy is for the domain minus the amount of notification you wish to give the users. The next variable is a counter variable we will use to tell us how many users have passwords coming up for renewal. Remember in Windows PowerShell that all variables begin with the dollar sign. This is seen here. <\/p>\n<pre class=\"codeSample\">$MaxPasswordAge = 30\n$userCount = 0\n<\/pre>\n<p>Now we need to create the <b>DirectoryServices.DirectorySearcher<\/b> object. You will see the <b>DirectorySearcher<\/b> class come up again this week. This .NET Framework class specializes in searching Active Directory Domain Services (AD DS). To create the class, we use the <b>New-Object<\/b> cmdlet to give it the name of the class, and tell it where we wish to connect. We wish to use the LDAP protocol, and connect to <b>rootdse<\/b>. This allows you to run the script without having to worry about the domain name. We store the returned <b>DirectorySearcher<\/b> object in the variable called <b>$adsiSearcher<\/b>. This is shown here:<\/p>\n<pre class=\"codeSample\">$adsiSearcher = new-object DirectoryServices.DirectorySearcher(\"LDAP:\/\/rootdse\")<\/pre>\n<p>We create our filter. This filter is very similar to the ones you would have used in VBScript. As a good exercise, you may want to refer to some of the <a href=\"http:\/\/www.microsoft.com\/technet\/scriptcenter\/scripts\/default.mspx?mfr=true\" target=\"_blank\">search AD DS<\/a> scripts from the Script Repository and adapt some of those search filters to this script. Our filter is nothing fancy; we simply choose the <b>objectCategory<\/b> attribute from AD DS if the value of the attribute is equal to <b>user<\/b>. Do not allow any spaces to slip in, or you will regret it. Big time. Trust me. The code is here:<\/p>\n<pre class=\"codeSample\">$adsiSearcher.filter = \"objectCategory=user\"<\/pre>\n<p>We then use the <b>findall<\/b> method from the <b>DirectorySearcher<\/b> object. We could have used other methods, such as <b>findone<\/b>, but that is not as exciting as finding all the users. We pipeline the results of the method call as shown here: <\/p>\n<pre class=\"codeSample\">$adsiSearcher.findall() |<\/pre>\n<p>Now we cheat a little. Remember that magic phrase from VBScript days? You know what I am talking about: <b>On Error Resume Next<\/b>. There was a reason for it hanging around the top of some scripts (such as they would not run otherwise.) Well, we are not cheating that much. The problem is that when you run this script without the <b>-ErrorAction<\/b> &#8220;silentlycontinue&#8221; you are confronted by a bunch of red error messages. They are not related to the script itself, but are related to using the <b>InvokeGet<\/b> method, which we will get to later. When you try to call the method on an empty attribute, it generates an error.<\/p>\n<p>Anyway, I spent way too much time looking at fish pictures from my dive vacation to the Little Cayman islands, and I did not have time to figure out how to trap the error. I justified it by realizing I need to tell my loyal readers they can specify the <b>-ErrorAction<\/b> parameter on every single cmdlet. That is right, it is what is called a common parameter. It is commonly found on all the cmdlets. There are actually four levels you can specify for the <b>ErrorAction<\/b>: <b>continue<\/b>, <b>silentlyContinue<\/b>, <b>inquire<\/b>, and <b>stop<\/b>. <b>Continue<\/b> is the default action, which means the script will try to continue running, but it will let you know about the errors. <b>SilentlyContinue<\/b> is the same as <b>On Error Resume Next<\/b>. It is the &#8220;forgetaboutit&#8221; level. Or as my Australian buddies say (hi Brett, Jit, Chris, and Pete), &#8220;no worries mate&#8221;: <\/p>\n<pre class=\"codeSample\">Foreach-Object -ErrorAction \"silentlycontinue\" `<\/pre>\n<p>We next specify the <b>-Begin<\/b> parameter of the <b>Foreach-Object<\/b> cmdlet. This section of code is only run one time. Here we use this occasion to print out our header message. Note the backtick, which is for line continuation. <\/p>\n<pre class=\"codeSample\">-Begin { \"The following users need to set their  password\" } `<\/pre>\n<p>The main part of the code occurs within the <b>-Process<\/b> section of the <b>Foreach-Object<\/b> cmdlet. The code here will run once for each item that comes across the pipeline. The object we get back from the <b>DirectorySearcher<\/b> is a <b>SearchResult<\/b> object which has a path property. We can use the path property to obtain a <b>DirectoryEntry<\/b> class. What does this really mean? It means that the results from the search are pretty well useless for us except that we can get the path, and use it to connect to the actual object in AD DS. This is what we are doing with this code: <b>[adsi]$_.path<\/b>. The <b>$_<\/b> is an automatic variable that refers to the current object on the pipeline. Because it is an object, it has methods and properties. It is a <b>SearchResult<\/b> object. It has a path property. We use the path property and feed it to the <b>[adsi]<\/b> type accelerator. This will allow us to get a <b>DirectoryEntry<\/b> class. We use the <b>.psbase<\/b> to gain access to the base object, which has the <b>InvokeGet<\/b> method. The <b>InvokeGet<\/b> method is used to retrieve the <b>PasswordLastChanged<\/b> property from AD DS. We store this value in the <b>$pwdChanged<\/b> variable. This is seen&nbsp;here:<\/p>\n<pre class=\"codeSample\">-Process `\n{ \n $pwdChanged = ([adsi]$_.path).psbase.InvokeGet(\"PasswordLastChanged\")\n<\/pre>\n<p>Now we use the <b>Get-Date<\/b> cmdlet to retrieve the current date and time. This actually returns a <b>system.datetime<\/b> class. We subtract the <b>$pwdChanged datetime<\/b> object from the current <b>datetime<\/b>, and select only the <b>days<\/b> property. If the number of days is greater than or equal to the value we set in the <b>$maxPasswordAge<\/b> variable, we print out the name of the object and increment the value in the <b>$userCount<\/b> variable by one. We do this by the convenient (if obscure) syntax of <b>$userCount ++<\/b>, which means take the value stored in the <b>$userCount<\/b> variable and increment it by one. This is seen&nbsp;here:<\/p>\n<pre class=\"codeSample\">If( ((get-date) - $pwdChanged).days -ge $MaxPasswordAge)\n   { \n    ([adsi]$_.path).name \n    $userCount ++\n   } #end if date\n} `\n<\/pre>\n<p>The total number of users that need to change their password is stored in the <b>$userCount<\/b> variable. We print this out after everything else is done. The <b>-end<\/b> parameter of the <b>Foreach-Object<\/b> cmdlet allows us to run post processing if we wish. This is seen here:<\/p>\n<pre class=\"codeSample\">-end { \"A total of $userCount users\" }<\/pre>\n<p>Well, AM, that is about it. See you tomorrow.<\/p>\n<p><font class=\"Apple-style-span\" face=\"Verdana\" size=\"3\"><span class=\"Apple-style-span\"><b><b>Ed Wilson and Craig Liebendorfer, Scripting Guys<\/b><\/b><\/span><\/font><\/p>\n<p><font class=\"Apple-style-span\" face=\"Verdana\" size=\"3\"><b><\/b><\/font><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hey, Scripting Guy! The pointy headed boss (PHB) is at it again. He has decided he does not like it when users do not change their password when prompted. As a result, he wants me to produce a report once a week of all the users who will have to change their password next week. [&hellip;]<\/p>\n","protected":false},"author":595,"featured_media":87096,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[7,3,8,20,45],"class_list":["post-54873","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-scripting","tag-active-directory","tag-scripting-guy","tag-searching-active-directory","tag-user-accounts","tag-windows-powershell"],"acf":[],"blog_post_summary":"<p>Hey, Scripting Guy! The pointy headed boss (PHB) is at it again. He has decided he does not like it when users do not change their password when prompted. As a result, he wants me to produce a report once a week of all the users who will have to change their password next week. [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/54873","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/users\/595"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/comments?post=54873"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/54873\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media\/87096"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media?parent=54873"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=54873"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=54873"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}