PowerShell v5.1 Released

Microsoft has released the Windows Management Framework (WMF) 5.1, including Windows PowerShell 5.1,  to the web. You can download it here. This is the final version that released with Windows Server 2016, though it doesn't include all the features of PowerShell 5.1 that are on Server 2016 because some are not supported on earlier versions of Windows. WMF 5.1 is available for Windows Server 2012 R2, Windows Server 2012, Windows 2008 R2 SP1, Windows 8.1, and Windows 7 SP1. (Note, this does NOT include Windows 8.0!)

 

Installation on Windows 7 and Windows Server 2008 R2 has updated installation requirements. Please carefully read the Release Notes before installing.

 

All that being said, I'm updating all my computers to the latest version. My Windows 10 and Server 2016 computers are already at the WMF 5.1 level, of course, but I still have some legacy servers that need updates. They'll be getting them over the next couple of weeks, and my lab image templates are getting updates as well.

PowerShell: Sending password expiration notices via GMail – Part 3

In Part 1 of this series, I showed you how to identify users whose password was about to expire. Then in Part 2 of the series, I took that list of users and sent email to them using gmail-hosted company email. This third part of the series pulls all that together into a single script, complete with comment-based help. As always, this and all my scripts are copyrighted, but you're welcome to use them as the basis for your own scripts. However, I do appreciate attribution. Thanks, and enjoy.

<#
.Synopsis
Sends a "Password Expiring" warning email through TreyResearch's gmail. 
.Description
Send-TreyPasswordExpiryNotice first creates a list of accounts whose password will expire in the 
near future (default is 1 week). It then emails the users to warn them that their password will expire soon. 

This initial version runs interactively only. 
.Example
Send-TreyPasswordExpiryNotice
Sends a warning notice to all TreyResearch users whose password will expire in the next 7 days or less.
.Example
Send-TreyPasswordExpiryNotice -Logging
Sends a warning notice to all TreyResearch users whose password will expire in the next 7 days or less, and 
creates a log file that is echoed to the console at the end. 
.Example
Send-TreyPasswordExpiryNotice -DaysWarning 14
Sends a warning notice to all TreyResearch users whose password will expire in the next 14 days or less.
.Example
Send-TreyPasswordExpiryNotice -DaysWarning 5 -Logging -Testing -Verbose
Does NOT send a warning notice to TreyResearch users, but rather processes the first user and sends a notice
to the admin user(s) and writes to the log file. The -Verbose switch will make it additionally chatty.
.Parameter DaysWarning
The number of days advanced warning to give users whose passwords are close to expiration. 
The default is 7 days or less. 
.Parameter Logging
Switch to enable logging. Logs are written to C:\Temp\emaillogs.csv. When this switch is true, 
Send-TreyPasswordExpiryNotice outputs a table with a list of accounts due to expire as well as 
writing to a log file. 
.Parameter Testing
Switch to enable testing. When enabled, email is sent to a list of Admin users and only a single account is processed. 
.Inputs
[int]
[switch]
[Switch]
.Notes
    Author: Charlie Russel
  ThanksTo: Robert Pearman (WSSMB MVP),Jeffrey Hicks (PS MVP)
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 06 Sept, 2016 (cpr)
          : 09 Dec,  2016 (cpr) -(Ver 1.5) -- Reworked: Only process users who need reminding. Formatting changes
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0)]
     [int]
     $DaysWarning = 7, 
     [parameter(Mandatory=$false)]
     [Switch]
     $Logging,
     [parameter(Mandatory=$false)]
     [switch]
     $Testing
     )


#Set parameters for gmail.
$smtpServer  ="smtp.gmail.com"
$SMTPPort    = 587
$from        = "IT Notification <it-notification@TreyResearch.net>"
$AdminUser1  = "Charlie.Russel@TreyResearch.net"
$AdminUser2  = "admin.user2@TreyResearch.net"
$AdminUser3  = "admin.user3@TreyResearch.net"
$externalUser= "external.account@example.com"

# Cast this to a list of strings to allow for multiple test recipients
[string[]]$testRecipient = $AdminUser1,$AdminUser2,$AdminUser3

<#
 This uses a stored password sitting on a local hard drive. This is a reasonably
 secure way to work with passwords in a file, and is ONLY accessible by the user that created 
 it. Create the password with: 
   
   PSH> Read-Host -AsSecureString | ConvertFrom-SecureString | Out-File $home\Documents\TreyPW.txt

 See blog post at: http://blogs.msmvps.com/russel/2016/10/04/powershell-get-credential-from-a-file for
 full details. 

 Alternately, simply prompt for the credentials here with Get-Credential.

#>

$TreyUsr = "charlie.russel@TreyResearch.net"
$TreyPW = Get-Content $Home\Documents\TreyPW.txt | ConvertTo-SecureString
$Cred = New-Object System.Management.Automation.PSCredential -ArgumentList $TreyUsr, $TreyPW

 
# Check Logging Settings 
if ($Logging) { 
   $logFile = "C:\Temp\emaillogs.csv"
   if (! (Test-Path "C:\Temp") ) {
      Write-Verbose "No C:\Temp directory, so creating one..."
      New-Item -Path "C:\" -Name Temp -ItemType Directory
   }

    # Remove Logfile if it already exists
    If ( (Test-Path $logFile)) { 
      Remove-Item $logFile 
    }
    # Create CSV File and Headers 
    New-Item -Path $logfile -ItemType File 
    Add-Content $logfile "Date,Name,EmailAddress,DaysLeft,ExpiresOn,Notified" 
} 

# System Settings 
$textEncoding = [System.Text.Encoding]::UTF8 
$date = Get-Date -format "MM/dd/yyyy"

# Explicitly import the Active Directory module, but get rid of the noise if it's already loaded. 
Import-Module ActiveDirectory 4>$NULL


# Use the following to query the domain for who the PDC Emulator role holder is. 
$TreyDC = (Get-ADDomain -Identity "TreyResearch.net" -Credential $Cred).PDCEmulator

# Send a cc: to myself or a list of users
$AdminUser = "charlie.russel@TreyResearch.net"
$cclist = @($AdminUser)

# Do calculations outside the ForEach loop whenever possible
$maxPasswordAge = (Get-ADDefaultDomainPasswordPolicy -Server $TreyDC -Credential $Cred).MaxPasswordAge
$today = (get-date) 

# Notice this doesn't get Expired or NeverExpires users. Don't want to send them emails.  
$TreyUsers = Get-ADUser -filter * `
                    -properties Name,PasswordNeverExpires,PasswordExpired,PasswordLastSet,EmailAddress `
                    -Server $TreyDC `
                    -Credential $Cred `
         | where { $_.Enabled -eq $True `
             -AND  $_.PasswordNeverExpires -eq $False `
             -AND  $_.passwordexpired -eq $False `
             -AND  $_.EMailAddress `
             -AND  (($today - $_.PasswordLastSet).Days -ge ($MaxPasswordAge.Days - $DaysWarning))
         }
<# Get notification credentials. Prompt with Get-Credential if not using stored creds. 
$gCred = Get-Credential -UserName "it-notification@TreyResearch.net" `
                        -Message  "Enter Password for IT-Notification account"
#>
$gUsr = "it-notification@TreyResearch.net"
$gPW = Get-Content "$Home\Documents\itnotificationsPW.txt" | ConvertTo-SecureString
$gCred = New-Object System.Management.Automation.PSCredential -ArgumentList $gUsr, $gPW

# Now, we start to do the work. 
foreach ($user in $TreyUsers) { 
    Write-Verbose "Processing user $user"
    $Name = $user.Name 
    $Email = $user.emailaddress 
    $SAM = $user.SAMAccountName
    $sent = " " 
    $passwordSetDate = $user.PasswordLastSet 
    Write-Verbose "$SAM last set their password on $PasswordSetDate"

    $expiresOn = $passwordSetDate + $maxPasswordAge 
    $DaysLeft = (New-TimeSpan -Start $today -End $Expireson).Days 
 
    if (($DaysLeft) -gt "1") { 
        $MessageDays = "in " + "$DaysLeft" + " days." 
    } else { 
        $MessageDays = "today!" 
    } 
 
    # Email Subject Set Here 
    $subject="Your password will expire $messageDays" 
    Write-Verbose "$Name`'s password will expire $messageDays"
   
    # Email Body Set Here, Note You can use HTML, including Images. 
    # This uses PowerShell's here-string. 
$body =@" 
Dear $name, 
<p>Your TreyReseach.net Active Directory Domain credentials <b>will expire $messagedays</b> 
Please update your credentials as soon as possible! <br> </p>
 
<p>If you are using a Windows domain joined system and are connected to the intranet, 
press ctrl-alt-delete and select change password. Alternatively, if you are outside of the 
network, connect to the corporate VPN and reset your password with the same process.<br> </p>
 
<p>If you are not using a Windows based system, ensure you are on the intranet or connected to 
the corporate VPN.  Proceed to https://password.TreyResearch.net <https://password.TreyResearch.net> 
and reset your password.<br> </p>
 
<p>This process will also sync your newly created AD password to your Gmail password. Please 
allow up to 5 minutes for replication of the passwords to occur.<br><br> </p>
 
<p><br><b>Problems</b>? <br>Please open a Service Desk request by clicking on the 
Help Agent icon on your system. If you are NOT running a Help Agent, please contact a member
of the IT Team for instructions on how to install the agent. It is a strict TreyResearch 
company policy that all company-owned systems run the Help Agent. <br></p>
    
<p>Thanks, <br>  
IT Team
</P>
"@

    # If Testing Is Enabled - Email Administrator 
    if ($testing) { 
        $email = $testRecipient 
        $Subject = "PasswordExpiration Test Message"
        $cclist = $AdminUser2,$externalUser
    } 

   # Send Email Message 
    Write-Verbose "$SAM's password is due to expire in $DaysLeft which is less than the "
    Write-Verbose "DaysWarning Parameter setting of $DaysWarning days."

    # I've left this as a straight output to the host. If you want it quieter, make it a Write-Verbose
    "Sending Email Message to $email using $gUsr account"
    Send-Mailmessage -smtpServer $smtpServer `
                     -from $from `
                     -to $email `
                     -cc $cclist `
                     -subject $subject `
                     -body $body `
                     -bodyasHTML `
                     -priority High `
                     -Encoding $textEncoding `
                     -UseSSL `
                     -port $SMTPPort `
                     -Credential $gCred 
    $sent = "Yes"  # Used for logging
    if ($Logging) {
        Add-Content $logfile "$date,$Name,$email,$DaysLeft,$expiresOn,$Sent"  
    }
   if ($Testing) {
       "Sleeping 5, then breaking so we only process a single record"
       Sleep 5
       Break
    }
} 

If ($Logging) { 
   # Use the logging file to display a table of the accounts about to expire. 
   $expiringAccts = Import-Csv -Path $logfile
   $expiringAccts | Sort-Object -Property ExpiresOn `
                  | Format-Table -AutoSize `
                    @{Expression={$_.DaysLeft};`
                           Label="#Days";`
                           align="Right";`
                           width=7}, `
                    Name,`
                    @{Expression={(Get-Date -Date $_.ExpiresOn -Format 'MMMM dd')};`
                           Label="Expires On:"}
}

ETA: Minor bug fix (-Identity instead of -Identify. Sheesh!)

PowerShell: Sending password expiration notices via GMail – Part 2

In the first part of this trio of posts, I showed a way to identify users whose password was about to expire. Which is useful, but now you need to notify them. If your company email is in GMail, there's a few gotchas you'll need to watch out for, but I'll show you how to get it all working.

The process to email each of those users you identified in Part 1 has the following component parts:

  • Setup your log file if the -Logging switch was used
  • Get credentials for sending the email from a notification only address.
  • Extract the relevant user details for each user
  • Calculate the number of days before the user's password expires
  • Set the content of the email (subject, body, etc.)
  • Configure logging and testing behaviours
  • Send the actual email

First, to set up our logfile (a CSV file with details on each user we've sent email to):

if ($Logging) { 
   $logFile = 'C:\Temp\emaillogs.csv'
   if (! (Test-Path 'C:\Temp') ) {
      Write-Verbose 'No C:\Temp directory, so creating one...'
      New-Item -Path 'C:\' -Name 'Temp' -ItemType 'Directory'
   }

    # Remove Logfile if it already exists
    If ( (Test-Path $logFile)) { 
      Remove-Item $logFile 
    }
    # Create CSV File and Headers 
    New-Item -Path $logfile -ItemType File 
    Add-Content $logfile "Date,Name,EmailAddress,DaysLeft,ExpiresOn,Notified" 
}

Now there are certainly other was to do this, but this works, so I use it.

When you go to send the email, you'll likely want to use a notification only address, rather than your real one. But for that, you'll need to prompt for credentials. Or store them in a file, as described in an earlier post. To prompt, use:

$gCred = Get-Credential -UserName "it-notification@treyresearch.net" `
                        -Message  "Enter Password for IT-Notification account"

Now, let's start to build the ForEach loop you'll use to do the work of this process for each user. Start by extracting the relevant user details. (Note that $TreyUsers was built in Part 1 of this series).

foreach ($user in $TreyUsers) { 
    $Name  = $user.Name 
    $Email = $user.emailaddress 
    $Sam   = $user.SAMAccountName
    $sent  = "" # Reset Sent Flag 
    $passwordSetDate = $user.PasswordLastSet 
}

Now, continuing in that same ForEach loop, calculate how many days until their password expires, and set some text based on that:

$expiresOn = $passwordSetDate + $maxPasswordAge 
$DaysLeft  = (New-TimeSpan -Start $today -End $Expireson).Days 
         
if (($DaysLeft) -gt "1") { 
    $MessageDays = "in " + "$DaysLeft" + " days." 
} else { 
    $MessageDays = "today." 
}

You'll use that $MessageDays variable as you build your email message. That email message needs to tell them how long they have, and where and how to change their password. This last part is particularly important if you have users who regularly work remote, or whose primary work computer is a non-Windows computer. But even connected Windows users often have problems. :(

So, for the message subject you have:

$subject="Your password will expire $messageDays"

And for the message body, use something like this:

$body =@" 
Dear $name, 
<p>Your TreyReseach.net Active Directory Domain credentials <b>will expire $messagedays</b> 
Please update your credentials as soon as possible! <br> </p>

<p>If you are using a Windows domain joined system and are connected to the intranet, 
press ctrl-alt-delete and select change password. Alternatively, if you are outside of the 
network, connect to the corporate VPN and reset your password with the same process.<br> </p>

<p>If you are not using a Windows based system, ensure you are on the intranet or connected to 
the corporate VPN.  Proceed to https://password.TreyResearch.net <https://password.TreyResearch.net> 
and reset your password.<br> </p>

<p>This process will also sync your newly created AD password to your Gmail password. Please 
allow up to 5 minutes for replication of the passwords to occur.<br><br> </p>

<p><br><b>Problems</b>? <br>Please open a Service Desk request by clicking on the 
Help Agent icon on system. If you are NOT running a Help Agent, please contact a member
of the IT Team for instructions on how to install the agent. It is a strict TreyResearch 
company policy that all company-owned systems run the Help Agent. <br></p>
   
<p>Thanks, <br>  
IT Team
</P>
"@

Now you'll notice two things about that email body. First, it uses PowerShell's Here-String capability, making it easier to edit and even include double-quotes inside it if you want. You can do this without using a Here-String, but for any long text like this, it's easier in the long run. The second thing is that it includes some basic HTML codes. Gmail allows them, so use them for emphasis where appropriate.

 

Next, let's set up the processing of test messages and logging. First, for test messages, you'll want to override the user's email to only send it to yourself, or to your team. Still inside the ForEach loop, do that with:

[string[]]$testRecipient = $Charlie,$AdminUser2,$AdminUser3 
# If Testing Is Enabled--Email Admins, not real users, and cc an external address 
if ($testing) { 
    $email = $testRecipient 
    $Subject = "PasswordExpiration Test Message"
    $cclist = $externalUser
}

You'll also want to handle logging, so write the logging information to the log file you created earlier.

if ($Logging) {
    Add-Content $logfile "$date,$Name,$email,$DaysLeft,$expiresOn,$Sent"  
}

Finally, send the actual message:

Send-Mailmessage -smtpServer $smtpServer `
                 -from       $from `
                 -to         $email `
                 -cc         $cclist `
                 -subject    $subject `
                 -body       $body `
                 -bodyasHTML `
                 -priority   High `
                 -Encoding   $textEncoding `
                 -UseSSL `
                 -port       $SMTPPort `
                 -Credential $gCred

You'll notice a couple of things there. One, you need to identify your SMTP server and the SMTP port to use. That's usually smtp.gmail.com and port 587 for mail sent this way. Next, you need to have set the $textEncoding variable somewhere. In most cases, this will be UTF8, and you'll set that with:

$textEncoding = [System.Text.Encoding]::UTF8

Adjust as necessary for your environment.

 

Now, let's finish off by handling testing - you only want to send a single message to yourself if you're testing, not 60 of them! Do that with:

if ($Testing) {
   "Sleeping 5, then breaking so we only process a single record"
   Sleep 5
   Break
}

Finally, outside the ForEach loop, if logging is enabled, display the results of the logging file in a way you can read:

If ($Logging) { 
   # Use the logging file to display a table of the accounts about to expire. 
   $expiringAccts = Import-Csv -Path $logfile
   $expiringAccts | Sort-Object -Property ExpiresOn `
                  | Format-Table -AutoSize `
                    @{Expression={$_.DaysLeft};`
                           Label="#Days";`
                           align="Right";
                           width=7}, `
                    Name,`
                    @{Expression={(Get-Date -Date $_.ExpiresOn -Format 'MMMM dd')};`
                           Label="Expires On:"}
}

I'll post the entire script in Part 3, shortly, complete with comment-based help and full details.

PowerShell: Sending password expiration notices via GMail – Part 1

In a perfect world, users would never forget their password, and never forget to change it before the expiration date. But we don't live in that perfect world. I covered how to unlock AD accounts earlier in this post, but now I'd like to talk about how to first find the users whose accounts are about to expire, and then email a warning to them. It turns out to be a fairly big script, so I'm going to break it up into a couple of posts. In this first post, I'll cover how to identify the users whose password will expire in the next n days. Then in the next post, we'll send them an email via the company's GMail account. Finally, in the third post in this series, I'll pull the whole thing together into a complete script, with comment-based help.

 

The process to find the users breaks down into several component parts:

  • Get domain credentials
  • Connect to the AD DS domain
  • Do some Date arithmatic
  • Query Active Directory for the users whose passwords will expire

The first part, getting domain credentials, you can use a simple Get-Credential if you want to be prompted every time, or store the credentials securely in a file, as I described here. Notice that I don't suggest that you should simply log in as a Domain Admin and run the script with your domain credentials - that's because I much prefer to always run as a limited user.

 

After we have those credentials, we connect to the Active Directory domain and query it for the MaxPasswordAge property of the domain.

$ADCred = Get-Credential -UserName TreyResearch\Domain.Admin `
                         -Message "Enter the domain admin's password"
$PDC = (Get-Domain -Identity 'TreyResearch.net' `
                   -Credential $ADCred).PDCEmulator
$maxPasswordAge = (Get-ADDefaultDomainPasswordPolicy `
                         -Server $PDC `
                         -Credential $ADCred).MaxPasswordAge

Next, we're going to have to do some Date arithmetic. Rarely fun, but needs must. We'll start by getting some initial values. Our final script will assume a week of warning, but we'll want to be able to change that with a DaysWarning parameter:

[CmdletBinding()]
Param([Parameter(Mandatory=$False,Position=0)]
      [int]$DaysWarning = 7)

And we'll need to know today's date, that's easy:

$today = (Get-Date)

The other bit of information we need is the number of days since the user last set their password. We query for all users (-Filter *), but then discard all the ones we don't need, storing (in $TreyUsers) only those whose password will expire between now and the DaysWarning value. We don't need to do anything with those users whose password is set to never expire, nor do we care about users whose password has already expired. They won't be able to read any emails we send them anyway. :) While we're getting a list of these users, we need to get some properties that aren't returned by default when we use Get-ADUser. We'll use the -Properties parameter to specify those.

$TreyUsers = Get-ADUser -Filter * `
                        -Server $PDC `
                        -Credential $ADCred `
                        -Properties Name,`
                                    PasswordNeverExpires,`
                                    PasswordExpired,`
                                    PasswordLastSet,`
                                    EmailAddress `
      | Where-Object {$_.Enabled -eq $True `
          -AND $_.PasswordNeverExpires -eq $False `
          -AND $_.passwordexpired -eq $False `
          -AND $_.EMailAddress `
          -AND (($today-$_.PasswordLastSet).Days -ge ($MaxPasswordAge.Days-$DaysWarning))
      }

Whew, that looks a right mess. But it's not as bad as it looks. I've tried to make it as efficient as I could, taking advantage of PowerShell's 'short-cut' processing. Yes, I have to query AD DS for all the users, but I quickly stop processing the user if their account is disabled, their password never expires, or their password has already expired. That gets us down to only those users who are actually active. On those, we do a bit of date math. This is slightly complicated by the PasswordLastSet and MaxPasswordAge properties which return an object with more information than we need or want. All we really want is the Days property of those values.

 

The math here is a bit convoluted, so let's work through it. First ($today-$_.PasswordLastSet) is the number of days since the user set their password. The users we want to send an email to are those for whom that is greater than, or equal to,  MaxPasswordAge-DaysWarning.

 

How does that work? Let's assume we have a policy that says you need to change your password at least every 90 days, and I want to start warning users a week ahead of time.  Therefore, MaxPasswordAge-DaysWarning is equal to 83 days. So we only want to send warnings to those users who set their password more than 83 days ago.

 

Next time, we'll send each of those users an email, warning them that they need to change their password.

Getting Large Files

Sooner or later, you're likely to have to "clean up" a disk that's running out of space. One of the simplest ways to do that is to find the really large files on the disk or in a directory and delete ones that you don't actually need, or move them to a location that has more space. Over the years, I've used multiple tools to find the large files, but these days I use PowerShell.

To start, we need to use Get-ChildItem with the -Recurse parameter, and use Sort-Object to sort by the Length property (and to avoid problems if there's any errors, we'll tell it to just ignore errors and keep right on going).

Get-ChildItem -Path $Home -Recurse -ErrorAction SilentlyContinue  `
       | Sort-Object -Property Length -Descending

That's good, but obviously we only need to only return the big files, not all of them, so let's grab only the 10 largest files:

Get-ChildItem -Path $Home -Recurse -ErrorAction SilentlyContinue  `
       | Sort-Object -Property Length -Descending `
       | Select-Object -First 10

OK, better, but kind of hard to read. So, let's do some formatting and cleanup...

Get-ChildItem -Path $Home -Recurse -ErrorAction SilentlyContinue  `
       | Sort-Object -Property Length -Descending  `
       | Select-Object -First 10 `
       | Select-Object Name, `
           @{Label='SizeMB';Expression={"{0:N0}" -f ($_.Length/1MB)}},`
           @{Label='LastWrite';Expression={"{0:d}" -f ($_.LastWriteTime)}}, `
           DirectoryName

Now we're getting closer. But my standard PowerShell window is only 120 characters wide, and that leaves the directory getting chopped off:

Name                                                                              SizeMB LastWrite  DirectoryName
----                                                                              ------ ---------  -------------
871790_001_spp-2016.10.0-SPP2016100.2016_1015.191.iso                             6,672  2016-11-23 C:\Users\Charlie...
en_windows_server_2016_x64_dvd_9327751.iso                                        5,392  2016-10-23 C:\Users\Charlie...
en_windows_server_2012_r2_with_update_x64_dvd_6052708.iso                         5,148  2015-01-09 C:\Users\Charlie...
14393.0.160715-1616.RS1_RELEASE_SERVER_EVAL_X64FRE_EN-US.ISO                      5,076  2016-09-27 C:\Users\Charlie...
en_windows_server_2016_essentials_x64_dvd_9327792.iso                             4,473  2016-10-23 C:\Users\Charlie...
en_windows_storage_server_2016_x64_dvd_9327790.iso                                4,363  2016-10-23 C:\Users\Charlie...
CDR-X10_1.10_for_Intel_X10_platform.iso                                           4,312  2015-12-29 C:\Users\Charlie...
en_windows_10_multiple_editions_version_1511_updated_apr_2016_x64_dvd_8705583.iso 4,252  2016-06-03 C:\Users\Charlie...
en_windows_10_multiple_editions_version_1607_updated_jul_2016_x64_dvd_9058187.iso 4,177  2016-09-21 C:\Users\Charlie...
en_windows_10_multiple_editions_x64_dvd_6846432.iso                               3,895  2015-07-30 C:\Users\Charlie...

Which isn't terribly helpful whenI start trying to actually identify the files PowerShell has found. So, let's take advantage of Format-Table's ability to wrap lines:

Get-ChildItem -Path $Home -Recurse -ErrorAction SilentlyContinue  `
     | Sort-Object -Property Length -Descending  `
     | Select-Object -First 10 `
     | Select-Object Name, `
         @{Label='SizeMB';Expression={"{0:N0}" -f ($_.Length/1MB)}},`
         @{Label='LastWrite';Expression={"{0:d}" -f ($_.LastWriteTime)}}, `
         DirectoryName `
     | Format-Table -auto -wrap

Now that works a bit better, but it ends up with an awful lot of column width for the filename, and a really narrow column for the DirectoryName on my machine. So, let's take it the last step and set some column widths. We can't do that with the Select-Object expressions we've been using, but we can do it with Format-Table expressions:

Get-ChildItem -Path $Home -Recurse -ErrorAction SilentlyContinue  `
     | Sort-Object -Property Length -Descending  `
     | Select-Object -First 10 `
     | Select-Object Name, `
         @{Label='SizeMB';Expression={"{0:N0}" -f ($_.Length/1MB)}},`
         @{Label='LastWrite';Expression={"{0:d}" -f ($_.LastWriteTime)}}, `
         DirectoryName `
     | Format-Table -Wrap `
                     @{Label='File Name';Expression={$_.Name};Width=50},`
                     @{Label='SizeMB';Expression={$_.SizeMB};Width=7},`
                     @{Label='Last Write';Expression={$_.LastWrite};Width=11},`
                     DirectoryName

Now that's a useful display. Notice that when we got to Format-Table, the object names we wanted for our columns now matched the calculated column names, not the original Get-ChildItem property names.

So, let's take the whole thing and wrap it up in a script, with comment-based help, of course. We'll use two parameters -- the number of files to return, and the starting path.

<#
.Synopsis
Find the 10 largest files in a directory tree

.Description
Get-myLargeFiles does a recursive search of a directory and its subdirectories
to find the largest files in that directory tree. By default, it searches from the 
top of the $home directory, and returns the 10 largest files, but you can specify
the starting directory and the number of files to return on the command line. 

.Example
Get-myLargeFiles 

Returns the 10 largest files in your personal directory tree ($home).

.Example
Get-myLargeFiles -Path 'C:\' -Number 20

Returns the 20 largest files on the C: drive. Note that this will not report any failures
caused by insufficient permissions to traverse a particular directory tree. 

.Parameter $Path
The path to the top of the search tree. Default value is $home

.Parameter $Number
The number of large files to return. Default value is 10.

.Inputs
[string]
[Int]

.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 3/02/2016 (cpr)
   ModHist: 12/02/2016 (cpr) -- Set column widths in the Format-Table output. 
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0)]
     [string]
     $Path = $Home,
     [Parameter(Mandatory=$False,Position=1)]
     [int]
     $Number = 10
     )

Get-ChildItem $path -recurse -ea SilentlyContinue  `
       | Sort-Object Length -Descending  `
       | Select-Object -first $Number  `
       | Select-Object Name, `
           @{Label='SizeMB';Expression={"{0:N0}" -f ($_.Length/1MB)}},`
           @{Label='LastWrite';Expression={"{0:d}" -f ($_.LastWriteTime)}}, `
           DirectoryName `
       | Format-Table -Wrap `
             @{Label='File Name';Expression={$_.Name};Width=50},`
             @{Label='SizeMB';Expression={$_.SizeMB};Width=7},`
             @{Label='Last Write';Expression={$_.LastWrite};Width=11},`
             @{Label='Directory Name';Expression={$_.DirectoryName}}

Defaulting to PowerShell instead of CMD

Beginning in Windows 8.1, you could set the Windows PowerUser menu (right-click on the Start button, or Win-X key) to show Windows PowerShell and Windows PowerShell (Admin) on the menu instead of Command Prompt and Command Prompt (Admin). But every single new machine you log on to, you had to change that. A nuisance, at least. So, I created a Group Policy Preference to set the registry key for this, and linked this to the Default Domain Policy.

Apparently, Microsoft is finally catching up, and this is going to be the default on Windows 10 beginning with the build that's coming down today. About time!

For those of you who are not on the Fast Ring of Windows Insider builds, the registry key you need to set is:

HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\DontUsePowerShellOnWinX=0

To set that with Windows PowerShell, use:

Set-ItemProperty HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced `
                -Name DontUsePowerShellOnWinX `
                -Value 0 `
                -Type DWord

You can also use Group Policy Preferences to set that as part of a Group Policy Object (GPO).

    1. Open the Group Policy Management Console (gpmc.msc)
    2. Right-click the GPO you want to modify (I chose the Default Domain Policy for my domain)
    3. Select Edit from the right-click menu to open the Group Policy Editor
    4. Expand the User Configuration container, then Preferences and select Registry in the left pane.
    5. Right-click in the Registry details pane and select Registry Item from the New menu:
Adding a new registry item to Group Policy Preferences

Add a new registry item to Group Policy Preferences

6. In the New Registry Properties dialog, select Update for the Action, HKEY_CURRENT_USER for the Hive, and a Key Path of  \Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced.

Set PowerShell as the WinX default

Set PowerShell as the WinX default

7. The Value Name is DontUsePowerShellOnWinX, with a Value Type of REG_DWORD and a Value Data of 0, as shown below.

8. Click OK, and then close GPEdit. The Group Policy will be applied to following the next reboot and logon of each user to whom the GPO applies.

For those of you who insist that they really want CMD instead of PowerShell, you can simply set the value of that registry item to one (1) instead of zero (0). Or let users manually control it. But as I've been saying for years: "Death to CMD". :)

 

Using a list as a default parameter value

I got an interesting question in a comment today on my old post on Getting the Free Disk Space of Remote Computers. While I answered the comment in the original post, it brought up something that's not well understood, so I thought I'd take a moment to give a fuller answer here, where it will be more discoverable.

 

The reader complained that when he ran the script without parameters, he was getting an error. When I read the error, it was clear that he had modified the script to set a default value for the ComputerName parameter that was a list of computers, rather than a single computer. A reasonable change, since the script is set to handle a list or array of strings with [string[]]. And if you use the parameter with a list of -ComputerName 'server1','server2','server3', etc., from the command line, it handles that list as you would expect. But if you try to set a default value for the ComputerName parameter with that same list:

[CmdletBinding()]
Param ([Parameter(Mandatory=$False,Position=0)]
       [String[]]$ComputerName = 'server1','server2','server3')

You'll get an error:

Missing expression after ','

So, does this mean you can't set a default value that is a list? Of course not! But it does mean you need to let PowerShell know explicitly what you're doing. When you pass in an adhoc list like that from the command line, it just works because PowerShell does a fair amount of magic for things it sees on the command line. But in a script, we need to be more explicit. So, instead, we need to declare our list as... A List! We do that with:

[CmdletBinding()]
Param ([Parameter(Mandatory=$False,Position=0)]
       [String[]]$ComputerName = @('server1','server2','server3'))

Now, our script works as we'd expect. It accepts my default list of server1, server2, server3, but allows me to override that list with my own list at the command line. (And remember, a list can be a list of length one, having only a single member. )

Importing users into Active Directory

When you need to create a single user in Active Directory Domain Services (AD DS), the tendency is to just "do it" in the GUI - either Active Directory Users and Computers (ADUC) or Active Directory Administrative Center (ADAC). But if you've got 25 users to add, or even 5 users to add, that's just painful. Plus, having this scripted ensures that each user is correctly entered following the same format.

For me, the easiest way to do this is by putting the users in a spreadsheet and then leveraging PowerShell's Import-CSV command. So, for the sake of argument, let's assume I have 8 new users to add to my TreyResearch.net domain. I open Excel, and create a spreadsheet with the following information for each user: Name, Given Name, Surname, Display Name, SamAccountName, and Description. The spreadsheet would look like this:blog_01

We'll save that as "TreyUsers.csv", and ignore any warnings about formatting, etc. This gives us a file with:

Name,GivenName,Surname,DisplayName,SAMAccountName,Description
David Guy,David,Guy,Dave R. Guy,Dave,Customer Appreciation Manager
Alfredo Fettucine,Alfredo,Fettuccine,Alfie NoNose,Alfie,Shop Foreman
Stanley Behr,Stanley,Behr,Stanley T. Behr, Stanley,Webmaster
Priscilla Catz,Priscilla,Catz,Dame Priscilla,Priscilla,Shop Steward
Harold Catz,Harold,Catz,Harold S. Catz,Harold,Engineering Manager
William Wallace,William,Wallace,Sir William Wallace,Wally,Marketing Manager
Trey Barksdale,Trey,Barksdale,Lord Barksalot,Trey,Sales Manager
Charlie Derby,Charles,Derby,Sparky,Sparky,Chief Security Officer

Now, we use PowerShell's Import-CSV to import this set of users into a variable $TreyUsers.

$TreyUsers = Import-CSV TreyUsers.csv

To create the users, we'll use a simple ForEach loop:

ForEach ($user in $TreyUsers ) {
   New-AdUser -DisplayName $User.DisplayName `
              -Description $user.Description `
              -GivenName $user.GivenName `
              -Name $User.Name `
              -SurName $User.SurName `
              -SAMAccountName $User.SAMAccountName `
              -Enabled $True `
              -ChangePasswordAtLogon $True `
              -PasswordNeverExpires $False `
              -UserPrincipalName $user.SAMAccountName `
              -AccountPassword (ConvertTo-SecureString `
                   -AsPlainText `
                   -Force `
                   -String 'P@ssw0rd!' ) 2>$NULL
}

(If you find that plain text password a bit problematic, use a Read-Line -AsSecureString early in the script to prompt you for an initial password. )

Finally, if you want to copy the security groups of an existing template user, stay tuned -- I'll cover that shortly

 

ETA: To correct ill-advised use of double-quotes in ConvertTo-SecureString.

PowerShell: Get-Credential from a file

If you routinely have to log into a separate domain, it can be a nuisance to always have to run Get-Credential. Plus writing scripts with a -Credential parameter is a nuisance because if you call Get-Credential in the script, it will always prompt you.

 

I run a separate lab network here, with an Active Directory domain of TreyResearch.net. I got tired of always having scripts prompt me for credentials, or even more annoying, have routine PowerShell commands against computers in the lab fail because I didn't have credentials for that domain. The answer is pretty simple -- first, I stored my password securely in a file with:

Read-Host -AsSecureString `
         | ConvertFrom-SecureString `
         | Out-File $Home\Documents\WindowsPowerShell\TCred.txt

Now, I can use that password to create a PSCredential object that I can pass into a script.

$tPW = Get-Content $home\Documents\WindowsPowerShell\TCred.txt `
         | ConvertTo-SecureString
$tCred = New-Object -TypeName System.Management.Automation.PSCredential `
                    -ArgumentList "TreyResearch\Charlie",$tPW

 

Because of the way SecureString works, and how Windows encrypts and decrypts objects, this password can only be read from the account that created it. Now, if I want to add a -Credential parameter to my scripts, I use the following:

[CmdletBinding()]
Param([Parameter(Mandatory=$false,ValueFromPipeLine=$True)]
      [PSCredential]
      $Credential = $NULL
     )

if ( $Credential ) {
   $tCred = $Credential
} else {
   $tPW = Get-Content $home\Documents\WindowsPowerShell\TCred.txt `
        | ConvertTo-SecureString 
   $tCred = New-Object -TypeName System.Management.Automation.PSCredential `
                       -ArgumentList "TreyResearch\Charlie",$tPW
}

ETA: if you create a script for this, you'll need to "dot source" the script to add the credential to your environment, but you can use that script in the pipeline to insert a credential into the pipeline. Or, add the relevant lines into your $Profile, and the credential is then available in your environment.

Getting Automatic Services That Are Stopped With PowerShell

One of the first things I check when I am troubleshooting a system is whether all the services that should be running, are. I could just open up services.msc, click on the "Startup Type" column to sort by the startup type, and scroll down through the Automatic services to see which ones aren't running. But that's so…. GUI  :p. And slower, and so very one machine at a time. Instead, let's use PowerShell to make it all easier.

 

First, I checked Get-Service, thinking it would give me what I need. but it doesn't. There's no way with Get-Service to find out what the startup type is -- it's not a property returned by Get-Service. (Yes, I think this is a deficiency. And yes, I expect someday we might get an improvement to Get-Service. But for the moment, we have to work around it. )

 

Instead, I decided to use the Get-WmiObject cmdlet to find what we need. (If the machine you're running this from is running PowerShell v3 or later, you can substitute Get-CimInstance for Get-WmiObject. But if you do, you won't be able to use -Credential.)

 

Get-WmiObject Win32_Service returns a list of all the services on the local machine. We can extend it with -ComputerName to query the services on a remote computer. And we can filter those services, though the filtering uses WQL as the query language, which is a nuisance since it doesn't match up to the Filter syntax for the ActiveDirectory module, for example.

 

To get a list of all the services that should have started automatically, but that are not currently running, on the local machine:

Get-WmiObject -ClassName Win32_Service -Filter "StartMode='Auto' AND State<>'Running'"

But that output is a bit ugly, so we'll throw some Format-Table at it, and come up with:

Get-WmiObject -ClassName Win32_Service `
              -Filter "StartMode='Auto' AND State<>'Running'" `
             | Format-Table -Auto DisplayName,Name,StartMode,State

Not bad. That gives us an easy to read output with all the information we need. We can wrap that up in a simple cmdlet that assumes the local computer, but that allows us to run it against multiple computers. And we want it to be able to get that list of computer names through the pipeline, of course. Plus, we'll add a Credential parameter to allow us to run against machines on a different domain, or a workgroup, so long as we provide an appropriate credential.

 

If we're going to get output from multiple computers, however, we need to know which one has which services that aren't running. To do that, we take advantage of Format-Tables GroupBy parameter:

Get-WmiObject -ClassName Win32_Service `
              -Filter "StartMode='Auto' AND State<>'Running'" `
             | Format-Table -AutoSize `
                            -Property DisplayName,Name,StartMode,State `
                            -GroupBy  PSComputer

Now we have everything we need to pull our script together.

Get-myStoppedService.ps1

<#
.Synopsis
Gets a list of stopped services
.Description
Get-myStoppedService takes a list of computer names and returns 
a table of the stopped services on that computer that are set to 
automatically start. The default is to return a list on the local computer.
.Example
Get-myStoppedService
Returns a table of stopped services on the local computer
.Example
Get-myStoppedService -ComputerName 'server1','client2'
Returns a table of stopped services on server1 and client2, 
grouped by computer name
.Parameter ComputerName
A list of remote computer names to query. If the current account 
doesn't have permission to query WMI on the remote computer, use 
the Credential parameter to provide alternate credentials. 
The default is the local host.
.Parameter Credential
Standard PSCredential object. Use Get-Credential.
.Inputs
[string[]]
[PSCredential]
.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 29 September, 2016 (cpr)
   ModHist:
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0,ValueFromPipeline=$True)]
     [Alias("Name","VMName")]
     [string[]]
     $ComputerName = ".",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

if ($Credential) {
   Get-WMIObject -ClassName Win32_Service `
                 -Credential $Credential `
                 -ComputerName $ComputerName `
                 -Filter "StartMode='Auto' AND State<>'Running'" `
                | Format-Table -Auto DisplayName,Name,StartMode,State -GroupBy PSComputerName
} else {
   Get-WmiObject -ClassName Win32_Service `
                 -ComputerName $ComputerName `
                 -Filter "StartMode='Auto' AND State<>'Running'" `
                | Format-Table -Auto DisplayName,Name,StartMode,State -GroupBy PSComputerName
}